summaryrefslogtreecommitdiffstats
path: root/browser/components/places
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/places')
-rw-r--r--browser/components/places/PlacesUIUtils.jsm1774
-rw-r--r--browser/components/places/content/bookmarkProperties.js693
-rw-r--r--browser/components/places/content/bookmarkProperties.xul43
-rw-r--r--browser/components/places/content/bookmarksPanel.js24
-rw-r--r--browser/components/places/content/bookmarksPanel.xul54
-rw-r--r--browser/components/places/content/browserPlacesViews.js1996
-rw-r--r--browser/components/places/content/controller.js1742
-rw-r--r--browser/components/places/content/downloadsViewOverlay.xul47
-rw-r--r--browser/components/places/content/editBookmarkOverlay.js1168
-rw-r--r--browser/components/places/content/editBookmarkOverlay.xul188
-rw-r--r--browser/components/places/content/history-panel.js98
-rw-r--r--browser/components/places/content/history-panel.xul95
-rw-r--r--browser/components/places/content/menu.xml633
-rw-r--r--browser/components/places/content/moveBookmarks.js65
-rw-r--r--browser/components/places/content/moveBookmarks.xul53
-rw-r--r--browser/components/places/content/organizer.css7
-rw-r--r--browser/components/places/content/places.css25
-rw-r--r--browser/components/places/content/places.js1405
-rw-r--r--browser/components/places/content/places.xul438
-rw-r--r--browser/components/places/content/placesOverlay.xul233
-rw-r--r--browser/components/places/content/sidebarUtils.js106
-rw-r--r--browser/components/places/content/tree.xml801
-rw-r--r--browser/components/places/content/treeView.js1726
-rw-r--r--browser/components/places/jar.mn34
-rw-r--r--browser/components/places/moz.build18
-rw-r--r--browser/components/places/tests/browser/.eslintrc.js7
-rw-r--r--browser/components/places/tests/browser/bookmark_dummy_1.html9
-rw-r--r--browser/components/places/tests/browser/bookmark_dummy_2.html9
-rw-r--r--browser/components/places/tests/browser/browser.ini58
-rw-r--r--browser/components/places/tests/browser/browser_0_library_left_pane_migration.js90
-rw-r--r--browser/components/places/tests/browser/browser_410196_paste_into_tags.js114
-rw-r--r--browser/components/places/tests/browser/browser_416459_cut.js83
-rw-r--r--browser/components/places/tests/browser/browser_423515.js173
-rw-r--r--browser/components/places/tests/browser/browser_425884.js127
-rw-r--r--browser/components/places/tests/browser/browser_435851_copy_query.js59
-rw-r--r--browser/components/places/tests/browser/browser_475045.js65
-rw-r--r--browser/components/places/tests/browser/browser_555547.js66
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js53
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js110
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_addLivemark.js39
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js71
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js42
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_all_tabs.js37
-rw-r--r--browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js61
-rw-r--r--browser/components/places/tests/browser/browser_bookmarksProperties.js450
-rw-r--r--browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js256
-rw-r--r--browser/components/places/tests/browser/browser_forgetthissite_single.js78
-rw-r--r--browser/components/places/tests/browser/browser_history_sidebar_search.js64
-rw-r--r--browser/components/places/tests/browser/browser_library_batch_delete.js114
-rw-r--r--browser/components/places/tests/browser/browser_library_commands.js235
-rw-r--r--browser/components/places/tests/browser/browser_library_downloads.js70
-rw-r--r--browser/components/places/tests/browser/browser_library_infoBox.js197
-rw-r--r--browser/components/places/tests/browser/browser_library_left_pane_fixnames.js94
-rw-r--r--browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js41
-rw-r--r--browser/components/places/tests/browser/browser_library_middleclick.js279
-rw-r--r--browser/components/places/tests/browser/browser_library_openFlatContainer.js42
-rw-r--r--browser/components/places/tests/browser/browser_library_open_leak.js23
-rw-r--r--browser/components/places/tests/browser/browser_library_panel_leak.js54
-rw-r--r--browser/components/places/tests/browser/browser_library_search.js182
-rw-r--r--browser/components/places/tests/browser/browser_library_views_liveupdate.js300
-rw-r--r--browser/components/places/tests/browser/browser_markPageAsFollowedLink.js68
-rw-r--r--browser/components/places/tests/browser/browser_sidebarpanels_click.js157
-rw-r--r--browser/components/places/tests/browser/browser_sort_in_library.js249
-rw-r--r--browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js53
-rw-r--r--browser/components/places/tests/browser/browser_views_liveupdate.js475
-rw-r--r--browser/components/places/tests/browser/frameLeft.html8
-rw-r--r--browser/components/places/tests/browser/frameRight.html8
-rw-r--r--browser/components/places/tests/browser/framedPage.html9
-rw-r--r--browser/components/places/tests/browser/head.js460
-rw-r--r--browser/components/places/tests/browser/keyword_form.html17
-rw-r--r--browser/components/places/tests/browser/pageopeningwindow.html9
-rw-r--r--browser/components/places/tests/browser/sidebarpanels_click_test_page.html7
-rw-r--r--browser/components/places/tests/chrome/.eslintrc.js7
-rw-r--r--browser/components/places/tests/chrome/chrome.ini15
-rw-r--r--browser/components/places/tests/chrome/head.js7
-rw-r--r--browser/components/places/tests/chrome/test_0_bug510634.xul99
-rw-r--r--browser/components/places/tests/chrome/test_0_multiple_left_pane.xul85
-rw-r--r--browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xul89
-rw-r--r--browser/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul91
-rw-r--r--browser/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul83
-rw-r--r--browser/components/places/tests/chrome/test_bug549192.xul120
-rw-r--r--browser/components/places/tests/chrome/test_bug549491.xul78
-rw-r--r--browser/components/places/tests/chrome/test_bug631374_tags_selector_scroll.xul170
-rw-r--r--browser/components/places/tests/chrome/test_editBookmarkOverlay_keywords.xul99
-rw-r--r--browser/components/places/tests/chrome/test_editBookmarkOverlay_tags_liveUpdate.xul204
-rw-r--r--browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xul86
-rw-r--r--browser/components/places/tests/chrome/test_treeview_date.xul159
-rw-r--r--browser/components/places/tests/unit/.eslintrc.js7
-rw-r--r--browser/components/places/tests/unit/bookmarks.glue.html16
-rw-r--r--browser/components/places/tests/unit/bookmarks.glue.json1
-rw-r--r--browser/components/places/tests/unit/corruptDB.sqlitebin0 -> 32772 bytes
-rw-r--r--browser/components/places/tests/unit/distribution.ini27
-rw-r--r--browser/components/places/tests/unit/head_bookmarks.js133
-rw-r--r--browser/components/places/tests/unit/test_421483.js103
-rw-r--r--browser/components/places/tests/unit/test_PUIU_makeTransaction.js361
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js33
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_corrupt.js59
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js52
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js55
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_distribution.js125
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_migrate.js70
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_prefs.js240
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_restore.js62
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js285
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_urlbar_defaultbehavior_migration.js150
-rw-r--r--browser/components/places/tests/unit/test_clearHistory_shutdown.js181
-rw-r--r--browser/components/places/tests/unit/test_leftpane_corruption_handling.js174
-rw-r--r--browser/components/places/tests/unit/xpcshell.ini25
108 files changed, 22289 insertions, 0 deletions
diff --git a/browser/components/places/PlacesUIUtils.jsm b/browser/components/places/PlacesUIUtils.jsm
new file mode 100644
index 000000000..b25835a71
--- /dev/null
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -0,0 +1,1774 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
+ "resource:///modules/RecentWindow.jsm");
+
+// PlacesUtils exposes multiple symbols, so we can't use defineLazyModuleGetter.
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions",
+ "resource://gre/modules/PlacesTransactions.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
+ "resource://gre/modules/CloudSync.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+ "resource://services-sync/main.js");
+
+const gInContentProcess = Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+const FAVICON_REQUEST_TIMEOUT = 60 * 1000;
+// Map from windows to arrays of data about pending favicon loads.
+let gFaviconLoadDataMap = new Map();
+
+// copied from utilityOverlay.js
+const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
+
+// This function isn't public both because it's synchronous and because it is
+// going to be removed in bug 1072833.
+function IsLivemark(aItemId) {
+ // Since this check may be done on each dragover event, it's worth maintaining
+ // a cache.
+ let self = 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);
+}
+
+let InternalFaviconLoader = {
+ /**
+ * This gets called for every inner window that is destroyed.
+ * In the parent process, we process the destruction ourselves. In the child process,
+ * we notify the parent which will then process it based on that message.
+ */
+ observe(subject, topic, data) {
+ let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ this.removeRequestsForInner(innerWindowID);
+ },
+
+ /**
+ * Actually cancel the request, and clear the timeout for cancelling it.
+ */
+ _cancelRequest({uri, innerWindowID, timerID, callback}, reason) {
+ // Break cycle
+ let request = callback.request;
+ delete callback.request;
+ // Ensure we don't time out.
+ clearTimeout(timerID);
+ try {
+ request.cancel();
+ } catch (ex) {
+ Cu.reportError("When cancelling a request for " + uri.spec + " because " + reason + ", it was already canceled!");
+ }
+ },
+
+ /**
+ * Called for every inner that gets destroyed, only in the parent process.
+ */
+ removeRequestsForInner(innerID) {
+ for (let [window, loadDataForWindow] of gFaviconLoadDataMap) {
+ let newLoadDataForWindow = loadDataForWindow.filter(loadData => {
+ let innerWasDestroyed = loadData.innerWindowID == innerID;
+ if (innerWasDestroyed) {
+ this._cancelRequest(loadData, "the inner window was destroyed or a new favicon was loaded for it");
+ }
+ // Keep the items whose inner is still alive.
+ return !innerWasDestroyed;
+ });
+ // Map iteration with for...of is safe against modification, so
+ // now just replace the old value:
+ gFaviconLoadDataMap.set(window, newLoadDataForWindow);
+ }
+ },
+
+ /**
+ * Called when a toplevel chrome window unloads. We use this to tidy up after ourselves,
+ * avoid leaks, and cancel any remaining requests. The last part should in theory be
+ * handled by the inner-window-destroyed handlers. We clean up just to be on the safe side.
+ */
+ onUnload(win) {
+ let loadDataForWindow = gFaviconLoadDataMap.get(win);
+ if (loadDataForWindow) {
+ for (let loadData of loadDataForWindow) {
+ this._cancelRequest(loadData, "the chrome window went away");
+ }
+ }
+ gFaviconLoadDataMap.delete(win);
+ },
+
+ /**
+ * Remove a particular favicon load's loading data from our map tracking
+ * load data per chrome window.
+ *
+ * @param win
+ * the chrome window in which we should look for this load
+ * @param filterData ({innerWindowID, uri, callback})
+ * the data we should use to find this particular load to remove.
+ *
+ * @return the loadData object we removed, or null if we didn't find any.
+ */
+ _removeLoadDataFromWindowMap(win, {innerWindowID, uri, callback}) {
+ let loadDataForWindow = gFaviconLoadDataMap.get(win);
+ if (loadDataForWindow) {
+ let itemIndex = loadDataForWindow.findIndex(loadData => {
+ return loadData.innerWindowID == innerWindowID &&
+ loadData.uri.equals(uri) &&
+ loadData.callback.request == callback.request;
+ });
+ if (itemIndex != -1) {
+ let loadData = loadDataForWindow[itemIndex];
+ loadDataForWindow.splice(itemIndex, 1);
+ return loadData;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Create a function to use as a nsIFaviconDataCallback, so we can remove cancelling
+ * information when the request succeeds. Note that right now there are some edge-cases,
+ * such as about: URIs with chrome:// favicons where the success callback is not invoked.
+ * This is OK: we will 'cancel' the request after the timeout (or when the window goes
+ * away) but that will be a no-op in such cases.
+ */
+ _makeCompletionCallback(win, id) {
+ return {
+ onComplete(uri) {
+ let loadData = InternalFaviconLoader._removeLoadDataFromWindowMap(win, {
+ uri,
+ innerWindowID: id,
+ callback: this,
+ });
+ if (loadData) {
+ clearTimeout(loadData.timerID);
+ }
+ delete this.request;
+ },
+ };
+ },
+
+ ensureInitialized() {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+
+ Services.obs.addObserver(this, "inner-window-destroyed", false);
+ Services.ppmm.addMessageListener("Toolkit:inner-window-destroyed", msg => {
+ this.removeRequestsForInner(msg.data);
+ });
+ },
+
+ loadFavicon(browser, principal, uri) {
+ this.ensureInitialized();
+ let win = browser.ownerGlobal;
+ if (!gFaviconLoadDataMap.has(win)) {
+ gFaviconLoadDataMap.set(win, []);
+ let unloadHandler = event => {
+ let doc = event.target;
+ let eventWin = doc.defaultView;
+ if (eventWin == win) {
+ win.removeEventListener("unload", unloadHandler);
+ this.onUnload(win);
+ }
+ };
+ win.addEventListener("unload", unloadHandler, true);
+ }
+
+ let {innerWindowID, currentURI} = browser;
+
+ // Immediately cancel any earlier requests
+ this.removeRequestsForInner(innerWindowID);
+
+ // First we do the actual setAndFetch call:
+ let loadType = PrivateBrowsingUtils.isWindowPrivate(win)
+ ? PlacesUtils.favicons.FAVICON_LOAD_PRIVATE
+ : PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE;
+ let callback = this._makeCompletionCallback(win, innerWindowID);
+ let request = PlacesUtils.favicons.setAndFetchFaviconForPage(currentURI, uri, false,
+ loadType, callback, principal);
+
+ // Now register the result so we can cancel it if/when necessary.
+ if (!request) {
+ // The favicon service can return with success but no-op (and leave request
+ // as null) if the icon is the same as the page (e.g. for images) or if it is
+ // the favicon for an error page. In this case, we do not need to do anything else.
+ return;
+ }
+ callback.request = request;
+ let loadData = {innerWindowID, uri, callback};
+ loadData.timerID = setTimeout(() => {
+ this._cancelRequest(loadData, "it timed out");
+ this._removeLoadDataFromWindowMap(win, loadData);
+ }, FAVICON_REQUEST_TIMEOUT);
+ let loadDataForWindow = gFaviconLoadDataMap.get(win);
+ loadDataForWindow.push(loadData);
+ },
+};
+
+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",
+
+ /**
+ * Makes a URI from a spec, and do fixup
+ * @param aSpec
+ * The string spec of the URI
+ * @return 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() {
+ return [
+ this.DESCRIPTION_ANNO,
+ this.LOAD_IN_SIDEBAR_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 annos = [];
+ if (aData.annos) {
+ annos = aData.annos.filter(function (aAnno) {
+ return this._copyableAnnotations.includes(aAnno.name);
+ }, this);
+ }
+
+ // There's no need to copy the keyword since it's bound to the bookmark url.
+ return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(aData.uri),
+ aContainer, aIndex, aData.title,
+ null, 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.includes(aAnno.name);
+ }, 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.includes(aAnno.name)
+ }, this);
+ }
+
+ return new PlacesCreateLivemarkTransaction(feedURI, siteURI, aData.title,
+ aContainer, aIndex, annos);
+ },
+
+ /**
+ * 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.
+ * @return 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);
+ 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);
+ 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);
+ default:
+ if (type == PlacesUtils.TYPE_X_MOZ_URL ||
+ type == PlacesUtils.TYPE_UNICODE ||
+ type == TAB_DROP_TYPE) {
+ let title = type != PlacesUtils.TYPE_UNICODE ? data.title
+ : data.uri;
+ return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(data.uri),
+ container, index, title);
+ }
+ }
+ return null;
+ },
+
+ /**
+ * ********* PlacesTransactions version of the function defined above ********
+ *
+ * Constructs a Places Transaction for the drop or paste of a blob of data
+ * into a container.
+ *
+ * @param aData
+ * The unwrapped data blob of dropped or pasted data.
+ * @param aType
+ * The content type of the data.
+ * @param aNewParentGuid
+ * GUID of the container the data was dropped or pasted into.
+ * @param aIndex
+ * The index within the container the item was dropped or pasted at.
+ * @param aCopy
+ * The drag action was copy, so don't move folders or links.
+ *
+ * @return a Places Transaction that can be transacted for performing the
+ * move/insert command.
+ */
+ getTransactionForData: function(aData, aType, aNewParentGuid, aIndex, aCopy) {
+ if (!this.SUPPORTED_FLAVORS.includes(aData.type))
+ throw new Error(`Unsupported '${aData.type}' data type`);
+
+ if ("itemGuid" in aData) {
+ if (!this.PLACES_FLAVORS.includes(aData.type))
+ throw new Error (`itemGuid unexpectedly set on ${aData.type} data`);
+
+ let info = { guid: aData.itemGuid
+ , newParentGuid: aNewParentGuid
+ , newIndex: aIndex };
+ if (aCopy) {
+ info.excludingAnnotation = "Places/SmartBookmark";
+ return PlacesTransactions.Copy(info);
+ }
+ return PlacesTransactions.Move(info);
+ }
+
+ // Since it's cheap and harmless, we allow the paste of separators and
+ // bookmarks from builds that use legacy transactions (i.e. when itemGuid
+ // was not set on PLACES_FLAVORS data). Containers are a different story,
+ // and thus disallowed.
+ if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
+ throw new Error("Can't copy a container from a legacy-transactions build");
+
+ if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
+ return PlacesTransactions.NewSeparator({ parentGuid: aNewParentGuid
+ , index: aIndex });
+ }
+
+ let title = aData.type != PlacesUtils.TYPE_UNICODE ? aData.title
+ : aData.uri;
+ return PlacesTransactions.NewBookmark({ uri: NetUtil.newURI(aData.uri)
+ , title: title
+ , parentGuid: aNewParentGuid
+ , index: aIndex });
+ },
+
+ /**
+ * 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.includes("folderPicker");
+ // 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();
+ },
+
+ /**
+ * set and fetch a favicon. Can only be used from the parent process.
+ * @param browser {Browser} The XUL browser element for which we're fetching a favicon.
+ * @param principal {Principal} The loading principal to use for the fetch.
+ * @param uri {URI} The URI to fetch.
+ */
+ loadFavicon(browser, principal, uri) {
+ if (gInContentProcess) {
+ throw new Error("Can't track loads from within the child process!");
+ }
+ InternalFaviconLoader.loadFavicon(browser, principal, uri);
+ },
+
+ /**
+ * 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 <menu> 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 <META>
+ * element.
+ * @param doc
+ * A DOM Document to get a description for
+ * @return 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
+ * @return 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) {
+ // canUserRemove doesn't accept root nodes.
+ return false;
+ }
+
+ // 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 || 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(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/array;1"].
+ createInstance(Ci.nsIMutableArray);
+ args.appendElement(uriList, /* weak =*/ false);
+ 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 => {
+ let 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, {
+ allowPopups: aNode.uri.startsWith("javascript:"),
+ 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("OtherBookmarksFolderTitle"),
+ 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;
+ let corrupt = false;
+ 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.
+ corrupt = true;
+ break;
+ }
+
+ // Check that all queries have valid parents.
+ let parentId = bs.getFolderIdForItem(query.itemId);
+ if (!items.includes(parentId) && parentId != leftPaneRoot) {
+ // The parent is not part of the left pane, bail out and create a new
+ // left pane root.
+ corrupt = true;
+ 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++;
+ }
+
+ // Note: it's not enough to just check for queriesCount, since we may
+ // find an invalid query just after accounting for a sufficient number of
+ // valid ones. As well as we can't just rely on corrupt since we may find
+ // less valid queries than expected.
+ if (corrupt || 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);
+
+ 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
+ * @return 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] of Object.entries(this.leftPaneQueries)) {
+ if (aItemId == id)
+ queryName = name;
+ }
+ }
+ return queryName;
+ },
+
+ shouldShowTabsFromOtherComputersMenuitem: function() {
+ let weaveOK = Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED &&
+ Weave.Svc.Prefs.get("firstSync", "") != "notReady";
+ return weaveOK;
+ },
+
+ /**
+ * WARNING TO ADDON AUTHORS: DO NOT USE THIS METHOD. IT'S LIKELY TO BE REMOVED IN A
+ * FUTURE RELEASE.
+ *
+ * Checks if a place: href represents a folder shortcut.
+ *
+ * @param queryString
+ * the query string to check (a place: href)
+ * @return whether or not queryString represents a folder shortcut.
+ * @throws if queryString is malformed.
+ */
+ isFolderShortcutQueryString(queryString) {
+ // Based on GetSimpleBookmarksQueryFolder in nsNavHistory.cpp.
+
+ let queriesParam = { }, optionsParam = { };
+ PlacesUtils.history.queryStringToQueries(queryString,
+ queriesParam,
+ { },
+ optionsParam);
+ let queries = queries.value;
+ if (queries.length == 0)
+ throw new Error(`Invalid place: uri: ${queryString}`);
+ return queries.length == 1 &&
+ queries[0].folderCount == 1 &&
+ !queries[0].hasBeginTime &&
+ !queries[0].hasEndTime &&
+ !queries[0].hasDomain &&
+ !queries[0].hasURI &&
+ !queries[0].hasSearchTerms &&
+ !queries[0].tags.length == 0 &&
+ optionsParam.value.maxResults == 0;
+ },
+
+ /**
+ * WARNING TO ADDON AUTHORS: DO NOT USE THIS METHOD. IT"S LIKELY TO BE REMOVED IN A
+ * FUTURE RELEASE.
+ *
+ * Helpers for consumers of editBookmarkOverlay which don't have a node as their input.
+ * Given a partial node-like object, having at least the itemId property set, this
+ * method completes the rest of the properties necessary for initialising the edit
+ * overlay with it.
+ *
+ * @param aNodeLike
+ * an object having at least the itemId nsINavHistoryResultNode property set,
+ * along with any other properties available.
+ */
+ completeNodeLikeObjectForItemId(aNodeLike) {
+ if (this.useAsyncTransactions) {
+ // When async-transactions are enabled, node-likes must have
+ // bookmarkGuid set, and we cannot set it synchronously.
+ throw new Error("completeNodeLikeObjectForItemId cannot be used when " +
+ "async transactions are enabled");
+ }
+ if (!("itemId" in aNodeLike))
+ throw new Error("itemId missing in aNodeLike");
+
+ let itemId = aNodeLike.itemId;
+ let defGetter = XPCOMUtils.defineLazyGetter.bind(XPCOMUtils, aNodeLike);
+
+ if (!("title" in aNodeLike))
+ defGetter("title", () => PlacesUtils.bookmarks.getItemTitle(itemId));
+
+ if (!("uri" in aNodeLike)) {
+ defGetter("uri", () => {
+ let uri = null;
+ try {
+ uri = PlacesUtils.bookmarks.getBookmarkURI(itemId);
+ }
+ catch (ex) { }
+ return uri ? uri.spec : "";
+ });
+ }
+
+ if (!("type" in aNodeLike)) {
+ defGetter("type", () => {
+ if (aNodeLike.uri.length > 0) {
+ if (/^place:/.test(aNodeLike.uri)) {
+ if (this.isFolderShortcutQueryString(aNodeLike.uri))
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
+
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
+ }
+
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
+ }
+
+ let itemType = PlacesUtils.bookmarks.getItemType(itemId);
+ if (itemType == PlacesUtils.bookmarks.TYPE_FOLDER)
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER;
+
+ throw new Error("Unexpected item type");
+ });
+ }
+ },
+
+ /**
+ * Helpers for consumers of editBookmarkOverlay which don't have a node as their input.
+ *
+ * Given a bookmark object for either a url bookmark or a folder, returned by
+ * Bookmarks.fetch (see Bookmark.jsm), this creates a node-like object suitable for
+ * initialising the edit overlay with it.
+ *
+ * @param aFetchInfo
+ * a bookmark object returned by Bookmarks.fetch.
+ * @return a node-like object suitable for initialising editBookmarkOverlay.
+ * @throws if aFetchInfo is representing a separator.
+ */
+ promiseNodeLikeFromFetchInfo: Task.async(function* (aFetchInfo) {
+ if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR)
+ throw new Error("promiseNodeLike doesn't support separators");
+
+ return Object.freeze({
+ itemId: yield PlacesUtils.promiseItemId(aFetchInfo.guid),
+ bookmarkGuid: aFetchInfo.guid,
+ title: aFetchInfo.title,
+ uri: aFetchInfo.url !== undefined ? aFetchInfo.url.href : "",
+
+ get type() {
+ if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_FOLDER)
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER;
+
+ if (this.uri.length == 0)
+ throw new Error("Unexpected item type");
+
+ if (/^place:/.test(this.uri)) {
+ if (this.isFolderShortcutQueryString(this.uri))
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
+
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
+ }
+
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
+ }
+ });
+ }),
+
+ /**
+ * Shortcut for calling promiseNodeLikeFromFetchInfo on the result of
+ * Bookmarks.fetch for the given guid/info object.
+ *
+ * @see promiseNodeLikeFromFetchInfo above and Bookmarks.fetch in Bookmarks.jsm.
+ */
+ fetchNodeLike: Task.async(function* (aGuidOrInfo) {
+ let info = yield PlacesUtils.bookmarks.fetch(aGuidOrInfo);
+ if (!info)
+ return null;
+ return (yield this.promiseNodeLikeFromFetchInfo(info));
+ })
+};
+
+
+PlacesUIUtils.PLACES_FLAVORS = [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
+ PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
+ PlacesUtils.TYPE_X_MOZ_PLACE];
+
+PlacesUIUtils.URI_FLAVORS = [PlacesUtils.TYPE_X_MOZ_URL,
+ TAB_DROP_TYPE,
+ PlacesUtils.TYPE_UNICODE],
+
+PlacesUIUtils.SUPPORTED_FLAVORS = [...PlacesUIUtils.PLACES_FLAVORS,
+ ...PlacesUIUtils.URI_FLAVORS];
+
+XPCOMUtils.defineLazyServiceGetter(PlacesUIUtils, "RDF",
+ "@mozilla.org/rdf/rdf-service;1",
+ "nsIRDFService");
+
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() {
+ return Services.prefs.getComplexValue("intl.ellipsis",
+ Ci.nsIPrefLocalizedString).data;
+});
+
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "useAsyncTransactions", function() {
+ try {
+ return Services.prefs.getBoolPref("browser.places.useAsyncTransactions");
+ }
+ catch (ex) { }
+ return false;
+});
+
+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);
+});
+
+/**
+ * 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: (aName, aTransactions) =>
+ new PlacesAggregatedTransaction(aName, aTransactions),
+
+ createFolder: (aName, aContainer, aIndex, aAnnotations,
+ aChildItemsTransactions) =>
+ new PlacesCreateFolderTransaction(aName, aContainer, aIndex, aAnnotations,
+ aChildItemsTransactions),
+
+ createItem: (aURI, aContainer, aIndex, aTitle, aKeyword,
+ aAnnotations, aChildTransactions) =>
+ new PlacesCreateBookmarkTransaction(aURI, aContainer, aIndex, aTitle,
+ aKeyword, aAnnotations,
+ aChildTransactions),
+
+ createSeparator: (aContainer, aIndex) =>
+ new PlacesCreateSeparatorTransaction(aContainer, aIndex),
+
+ createLivemark: (aFeedURI, aSiteURI, aName, aContainer, aIndex,
+ aAnnotations) =>
+ new PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aName, aContainer,
+ aIndex, aAnnotations),
+
+ moveItem: (aItemId, aNewContainer, aNewIndex) =>
+ new PlacesMoveItemTransaction(aItemId, aNewContainer, aNewIndex),
+
+ removeItem: (aItemId) =>
+ new PlacesRemoveItemTransaction(aItemId),
+
+ editItemTitle: (aItemId, aNewTitle) =>
+ new PlacesEditItemTitleTransaction(aItemId, aNewTitle),
+
+ editBookmarkURI: (aItemId, aNewURI) =>
+ new PlacesEditBookmarkURITransaction(aItemId, aNewURI),
+
+ setItemAnnotation: (aItemId, aAnnotationObject) =>
+ new PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject),
+
+ setPageAnnotation: (aURI, aAnnotationObject) =>
+ new PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject),
+
+ editBookmarkKeyword: (aItemId, aNewKeyword) =>
+ new PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword),
+
+ editLivemarkSiteURI: (aLivemarkId, aSiteURI) =>
+ new PlacesEditLivemarkSiteURITransaction(aLivemarkId, aSiteURI),
+
+ editLivemarkFeedURI: (aLivemarkId, aFeedURI) =>
+ new PlacesEditLivemarkFeedURITransaction(aLivemarkId, aFeedURI),
+
+ editItemDateAdded: (aItemId, aNewDateAdded) =>
+ new PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded),
+
+ editItemLastModified: (aItemId, aNewLastModified) =>
+ new PlacesEditItemLastModifiedTransaction(aItemId, aNewLastModified),
+
+ sortFolderByName: (aFolderId) =>
+ new PlacesSortFolderByNameTransaction(aFolderId),
+
+ tagURI: (aURI, aTags) =>
+ new PlacesTagURITransaction(aURI, aTags),
+
+ untagURI: (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.
+ * @return 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.
+ * @return 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: () =>
+ PlacesUtils.transactionManager.beginBatch(null),
+
+ endBatch: () =>
+ PlacesUtils.transactionManager.endBatch(false),
+
+ doTransaction: (txn) =>
+ PlacesUtils.transactionManager.doTransaction(txn),
+
+ undoTransaction: () =>
+ PlacesUtils.transactionManager.undoTransaction(),
+
+ redoTransaction: () =>
+ PlacesUtils.transactionManager.redoTransaction(),
+
+ get numberOfUndoItems() {
+ return PlacesUtils.transactionManager.numberOfUndoItems;
+ },
+ get numberOfRedoItems() {
+ return PlacesUtils.transactionManager.numberOfRedoItems;
+ },
+ get maxTransactionCount() {
+ return PlacesUtils.transactionManager.maxTransactionCount;
+ },
+ set maxTransactionCount(val) {
+ PlacesUtils.transactionManager.maxTransactionCount = val;
+ },
+
+ clear: () =>
+ PlacesUtils.transactionManager.clear(),
+
+ peekUndoStack: () =>
+ PlacesUtils.transactionManager.peekUndoStack(),
+
+ peekRedoStack: () =>
+ PlacesUtils.transactionManager.peekRedoStack(),
+
+ getUndoStack: () =>
+ PlacesUtils.transactionManager.getUndoStack(),
+
+ getRedoStack: () =>
+ PlacesUtils.transactionManager.getRedoStack(),
+
+ AddListener: (aListener) =>
+ PlacesUtils.transactionManager.AddListener(aListener),
+
+ RemoveListener: (aListener) =>
+ PlacesUtils.transactionManager.RemoveListener(aListener)
+ }
+});
diff --git a/browser/components/places/content/bookmarkProperties.js b/browser/components/places/content/bookmarkProperties.js
new file mode 100644
index 000000000..afcf65736
--- /dev/null
+++ b/browser/components/places/content/bookmarkProperties.js
@@ -0,0 +1,693 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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"
+ * @ node (an nsINavHistoryResultNode object) - a node representing
+ * the bookmark.
+ * - "folder" (also applies to livemarks)
+ * @ node (an nsINavHistoryResultNode object) - a node representing
+ * 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"
+ * - "folderPicker" - hides both the tree and the menu.
+ *
+ * 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");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.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,
+
+ /**
+ * 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() {
+ let 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
+ this._node = dialogInfo.node;
+ this._title = this._node.title;
+ if (PlacesUtils.nodeIsFolder(this._node))
+ this._itemType = BOOKMARK_FOLDER;
+ else if (PlacesUtils.nodeIsURI(this._node))
+ this._itemType = BOOKMARK_ITEM;
+ }
+ },
+
+ /**
+ * 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* () {
+ 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:
+ gEditItemOverlay.initPanel({ node: this._node
+ , hiddenRows: this._hiddenRows
+ , focusedElement: "first" });
+ acceptButton.disabled = gEditItemOverlay.readOnly;
+ break;
+ case ACTION_ADD:
+ this._node = yield this._promiseNewItem();
+ // Edit the new item
+ gEditItemOverlay.initPanel({ node: this._node
+ , hiddenRows: this._hiddenRows
+ , postData: this._postData
+ , focusedElement: "first" });
+
+ // 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.
+ let locationField = this._element("locationField");
+ if (locationField.value == "about:blank")
+ locationField.value = "";
+
+ // 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 (!gEditItemOverlay.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);
+ }
+ }
+ }
+ }),
+
+ // nsIDOMEventListener
+ handleEvent: function BPP_handleEvent(aEvent) {
+ var target = aEvent.target;
+ switch (aEvent.type) {
+ case "input":
+ if (target.id == "editBMPanel_locationField" ||
+ 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;
+ }
+ },
+
+ // Hack for implementing batched-Undo around the editBookmarkOverlay
+ // instant-apply code. For all the details see the comment above beginBatch
+ // in browser-places.js
+ _batchBlockingDeferred: null,
+ _beginBatch() {
+ if (this._batching)
+ return;
+ if (PlacesUIUtils.useAsyncTransactions) {
+ this._batchBlockingDeferred = PromiseUtils.defer();
+ PlacesTransactions.batch(function* () {
+ yield this._batchBlockingDeferred.promise;
+ }.bind(this));
+ }
+ else {
+ PlacesUtils.transactionManager.beginBatch(null);
+ }
+ this._batching = true;
+ },
+
+ _endBatch() {
+ if (!this._batching)
+ return;
+
+ if (PlacesUIUtils.useAsyncTransactions) {
+ this._batchBlockingDeferred.resolve();
+ this._batchBlockingDeferred = null;
+ }
+ else {
+ PlacesUtils.transactionManager.endBatch(false);
+ }
+ this._batching = false;
+ },
+
+ // 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() {
+ // 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);
+ },
+
+ 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() {
+ // 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();
+ if (PlacesUIUtils.useAsyncTransactions)
+ PlacesTransactions.undo().catch(Components.utils.reportError);
+ else
+ 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);
+ }
+
+ // 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,
+ this._postData);
+
+ 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 (let uri of this._URIs) {
+ // uri should be an object in the form { url, title }. Though add-ons
+ // could still use the legacy form, where it's an nsIURI.
+ let [_uri, _title] = uri instanceof Ci.nsIURI ?
+ [uri, this._getURITitleFromHistory(uri)] : [uri.uri, uri.title];
+
+ let 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);
+ },
+
+ _createNewItem: Task.async(function* () {
+ let [container, index] = this._getInsertionPointDetails();
+ let txn;
+ switch (this._itemType) {
+ case BOOKMARK_FOLDER:
+ txn = this._getCreateNewFolderTransaction(container, index);
+ break;
+ case LIVEMARK_CONTAINER:
+ txn = new PlacesCreateLivemarkTransaction(this._feedURI, this._siteURI,
+ this._title, 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);
+
+ return Object.freeze({
+ itemId: this._itemId,
+ bookmarkGuid: bm.guid,
+ title: this._title,
+ uri: this._uri ? this._uri.spec : "",
+ type: this._itemType == BOOKMARK_ITEM ?
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_URI :
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER
+ });
+ }),
+
+ _promiseNewItem: Task.async(function* () {
+ if (!PlacesUIUtils.useAsyncTransactions)
+ return this._createNewItem();
+
+ let [containerId, index] = this._getInsertionPointDetails();
+ let parentGuid = yield PlacesUtils.promiseItemGuid(containerId);
+ let annotations = [];
+ if (this._description) {
+ annotations.push({ name: PlacesUIUtils.DESCRIPTION_ANNO
+ , value: this._description });
+ }
+ if (this._loadInSidebar) {
+ annotations.push({ name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO
+ , value: true });
+ }
+
+ let itemGuid;
+ let info = { parentGuid, index, title: this._title, annotations };
+ if (this._itemType == BOOKMARK_ITEM) {
+ info.url = this._uri;
+ if (this._keyword)
+ info.keyword = this._keyword;
+ if (this._postData)
+ info.postData = this._postData;
+
+ if (this._charSet && !PrivateBrowsingUtils.isWindowPrivate(window))
+ PlacesUtils.setCharsetForURI(this._uri, this._charSet);
+
+ itemGuid = yield PlacesTransactions.NewBookmark(info).transact();
+ }
+ else if (this._itemType == LIVEMARK_CONTAINER) {
+ info.feedUrl = this._feedURI;
+ if (this._siteURI)
+ info.siteUrl = this._siteURI;
+
+ itemGuid = yield PlacesTransactions.NewLivemark(info).transact();
+ }
+ else if (this._itemType == BOOKMARK_FOLDER) {
+ itemGuid = yield PlacesTransactions.NewFolder(info).transact();
+ for (let uri of this._URIs) {
+ let placeInfo = yield PlacesUtils.promisePlaceInfo(uri);
+ let title = placeInfo ? placeInfo.title : "";
+ yield PlacesTransactions.transact({ parentGuid: itemGuid, uri, title });
+ }
+ }
+ else {
+ throw new Error(`unexpected value for _itemType: ${this._itemType}`);
+ }
+
+ this._itemGuid = itemGuid;
+ this._itemId = yield PlacesUtils.promiseItemId(itemGuid);
+ return Object.freeze({
+ itemId: this._itemId,
+ bookmarkGuid: this._itemGuid,
+ title: this._title,
+ uri: this._uri ? this._uri.spec : "",
+ type: this._itemType == BOOKMARK_ITEM ?
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_URI :
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER
+ });
+ })
+};
diff --git a/browser/components/places/content/bookmarkProperties.xul b/browser/components/places/content/bookmarkProperties.xul
new file mode 100644
index 000000000..2c04f8b05
--- /dev/null
+++ b/browser/components/places/content/bookmarkProperties.xul
@@ -0,0 +1,43 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/"?>
+<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE dialog [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<dialog id="bookmarkproperties"
+ buttons="accept, cancel"
+ buttoniconaccept="save"
+ ondialogaccept="BookmarkPropertiesPanel.onDialogAccept();"
+ ondialogcancel="BookmarkPropertiesPanel.onDialogCancel();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="BookmarkPropertiesPanel.onDialogLoad();"
+ onunload="BookmarkPropertiesPanel.onDialogUnload();"
+ style="min-width: 30em;"
+ persist="screenX screenY width">
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="stringBundle"
+ src="chrome://browser/locale/places/bookmarkProperties.properties"/>
+ </stringbundleset>
+
+ <script type="application/javascript"
+ src="chrome://browser/content/places/editBookmarkOverlay.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/places/bookmarkProperties.js"/>
+
+<vbox id="editBookmarkPanelContent"/>
+
+</dialog>
diff --git a/browser/components/places/content/bookmarksPanel.js b/browser/components/places/content/bookmarksPanel.js
new file mode 100644
index 000000000..871d69725
--- /dev/null
+++ b/browser/components/places/content/bookmarksPanel.js
@@ -0,0 +1,24 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+function init() {
+ document.getElementById("bookmarks-view").place =
+ "place:queryType=1&folder=" + window.top.PlacesUIUtils.allBookmarksFolderId;
+}
+
+function searchBookmarks(aSearchString) {
+ var tree = document.getElementById('bookmarks-view');
+ if (!aSearchString)
+ tree.place = tree.place;
+ else
+ tree.applyFilter(aSearchString,
+ [PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.toolbarFolderId]);
+}
+
+window.addEventListener("SidebarFocused",
+ () => document.getElementById("search-box").focus(),
+ false);
diff --git a/browser/components/places/content/bookmarksPanel.xul b/browser/components/places/content/bookmarksPanel.xul
new file mode 100644
index 000000000..332b9e7a9
--- /dev/null
+++ b/browser/components/places/content/bookmarksPanel.xul
@@ -0,0 +1,54 @@
+<?xml version="1.0"?> <!-- -*- Mode: SGML; indent-tabs-mode: nil; -*- -->
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<!DOCTYPE page SYSTEM "chrome://browser/locale/places/places.dtd">
+
+<page id="bookmarksPanel"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="init();"
+ onunload="SidebarUtils.setMouseoverURL('');">
+
+ <script type="application/javascript"
+ src="chrome://browser/content/bookmarks/sidebarUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/bookmarks/bookmarksPanel.js"/>
+
+ <commandset id="placesCommands"/>
+ <commandset id="editMenuCommands"/>
+ <menupopup id="placesContext"/>
+
+ <!-- Bookmarks and history tooltip -->
+ <tooltip id="bhTooltip"/>
+
+ <hbox id="sidebar-search-container" align="center">
+ <label id="sidebar-search-label"
+ value="&search.label;" accesskey="&search.accesskey;" control="search-box"/>
+ <textbox id="search-box" flex="1" type="search" class="compact"
+ aria-controls="bookmarks-view"
+ oncommand="searchBookmarks(this.value);"/>
+ </hbox>
+
+ <tree id="bookmarks-view" class="sidebar-placesTree" type="places"
+ flex="1"
+ hidecolumnpicker="true"
+ context="placesContext"
+ onkeypress="SidebarUtils.handleTreeKeyPress(event);"
+ onclick="SidebarUtils.handleTreeClick(this, event, true);"
+ onmousemove="SidebarUtils.handleTreeMouseMove(event);"
+ onmouseout="SidebarUtils.setMouseoverURL('');">
+ <treecols>
+ <treecol id="title" flex="1" primary="true" hideheader="true"/>
+ </treecols>
+ <treechildren id="bookmarks-view-children" view="bookmarks-view"
+ class="sidebar-placesTreechildren" flex="1" tooltip="bhTooltip"/>
+ </tree>
+</page>
diff --git a/browser/components/places/content/browserPlacesViews.js b/browser/components/places/content/browserPlacesViews.js
new file mode 100644
index 000000000..c6ee9b6ce
--- /dev/null
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -0,0 +1,1996 @@
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * The base view implements everything that's common to the toolbar and
+ * menu views.
+ */
+function PlacesViewBase(aPlace, aOptions) {
+ this.place = aPlace;
+ this.options = aOptions;
+ this._controller = new PlacesController(this);
+ this._viewElt.controllers.appendController(this._controller);
+}
+
+PlacesViewBase.prototype = {
+ // The xul element that holds the entire view.
+ _viewElt: null,
+ get viewElt() {
+ return this._viewElt;
+ },
+
+ get associatedElement() {
+ return this._viewElt;
+ },
+
+ get controllers() {
+ return this._viewElt.controllers;
+ },
+
+ // The xul element that represents the root container.
+ _rootElt: null,
+
+ // Set to true for views that are represented by native widgets (i.e.
+ // the native mac menu).
+ _nativeView: false,
+
+ QueryInterface: XPCOMUtils.generateQI(
+ [Components.interfaces.nsINavHistoryResultObserver,
+ Components.interfaces.nsISupportsWeakReference]),
+
+ _place: "",
+ get place() {
+ return this._place;
+ },
+ set place(val) {
+ this._place = val;
+
+ let history = PlacesUtils.history;
+ let queries = { }, options = { };
+ history.queryStringToQueries(val, queries, { }, options);
+ if (!queries.value.length)
+ queries.value = [history.getNewQuery()];
+
+ let result = history.executeQueries(queries.value, queries.value.length,
+ options.value);
+ result.addObserver(this, false);
+ return val;
+ },
+
+ _result: null,
+ get result() {
+ return this._result;
+ },
+ set result(val) {
+ if (this._result == val)
+ return val;
+
+ if (this._result) {
+ this._result.removeObserver(this);
+ this._resultNode.containerOpen = false;
+ }
+
+ if (this._rootElt.localName == "menupopup")
+ this._rootElt._built = false;
+
+ this._result = val;
+ if (val) {
+ this._resultNode = val.root;
+ this._rootElt._placesNode = this._resultNode;
+ this._domNodes = new Map();
+ this._domNodes.set(this._resultNode, this._rootElt);
+
+ // This calls _rebuild through invalidateContainer.
+ this._resultNode.containerOpen = true;
+ }
+ else {
+ this._resultNode = null;
+ delete this._domNodes;
+ }
+
+ return val;
+ },
+
+ _options: null,
+ get options() {
+ return this._options;
+ },
+ set options(val) {
+ if (!val)
+ val = {};
+
+ if (!("extraClasses" in val))
+ val.extraClasses = {};
+ this._options = val;
+
+ return val;
+ },
+
+ /**
+ * Gets the DOM node used for the given places node.
+ *
+ * @param aPlacesNode
+ * a places result node.
+ * @throws if there is no DOM node set for aPlacesNode.
+ */
+ _getDOMNodeForPlacesNode:
+ function PVB__getDOMNodeForPlacesNode(aPlacesNode) {
+ let node = this._domNodes.get(aPlacesNode, null);
+ if (!node) {
+ throw new Error("No DOM node set for aPlacesNode.\nnode.type: " +
+ aPlacesNode.type + ". node.parent: " + aPlacesNode);
+ }
+ return node;
+ },
+
+ get controller() {
+ return this._controller;
+ },
+
+ get selType() {
+ return "single";
+ },
+ selectItems: function() { },
+ selectAll: function() { },
+
+ get selectedNode() {
+ if (this._contextMenuShown) {
+ let anchor = this._contextMenuShown.triggerNode;
+ if (!anchor)
+ return null;
+
+ if (anchor._placesNode)
+ return this._rootElt == anchor ? null : anchor._placesNode;
+
+ anchor = anchor.parentNode;
+ return this._rootElt == anchor ? null : (anchor._placesNode || null);
+ }
+ return null;
+ },
+
+ get hasSelection() {
+ return this.selectedNode != null;
+ },
+
+ get selectedNodes() {
+ let selectedNode = this.selectedNode;
+ return selectedNode ? [selectedNode] : [];
+ },
+
+ get removableSelectionRanges() {
+ // On static content the current selectedNode would be the selection's
+ // parent node. We don't want to allow removing a node when the
+ // selection is not explicit.
+ if (document.popupNode &&
+ (document.popupNode == "menupopup" || !document.popupNode._placesNode))
+ return [];
+
+ return [this.selectedNodes];
+ },
+
+ get draggableSelection() {
+ return [this._draggedElt];
+ },
+
+ get insertionPoint() {
+ // There is no insertion point for history queries, so bail out now and
+ // save a lot of work when updating commands.
+ let resultNode = this._resultNode;
+ if (PlacesUtils.nodeIsQuery(resultNode) &&
+ PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
+ return null;
+
+ // By default, the insertion point is at the top level, at the end.
+ let index = PlacesUtils.bookmarks.DEFAULT_INDEX;
+ let container = this._resultNode;
+ let orientation = Ci.nsITreeView.DROP_BEFORE;
+ let tagName = null;
+
+ let selectedNode = this.selectedNode;
+ if (selectedNode) {
+ let popup = document.popupNode;
+ if (!popup._placesNode || popup._placesNode == this._resultNode ||
+ popup._placesNode.itemId == -1 || !selectedNode.parent) {
+ // If a static menuitem is selected, or if the root node is selected,
+ // the insertion point is inside the folder, at the end.
+ container = selectedNode;
+ orientation = Ci.nsITreeView.DROP_ON;
+ }
+ else {
+ // In all other cases the insertion point is before that node.
+ container = selectedNode.parent;
+ index = container.getChildIndex(selectedNode);
+ if (PlacesUtils.nodeIsTagQuery(container)) {
+ tagName = container.title;
+ // TODO (Bug 1160193): properly support dropping on a tag root.
+ if (!tagName)
+ return null;
+ }
+ }
+ }
+
+ if (PlacesControllerDragHelper.disallowInsertion(container))
+ return null;
+
+ return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
+ index, orientation, tagName);
+ },
+
+ buildContextMenu: function PVB_buildContextMenu(aPopup) {
+ this._contextMenuShown = aPopup;
+ window.updateCommands("places");
+ return this.controller.buildContextMenu(aPopup);
+ },
+
+ destroyContextMenu: function PVB_destroyContextMenu(aPopup) {
+ this._contextMenuShown = null;
+ },
+
+ _cleanPopup: function PVB_cleanPopup(aPopup, aDelay) {
+ // Remove Places nodes from the popup.
+ let child = aPopup._startMarker;
+ while (child.nextSibling != aPopup._endMarker) {
+ let sibling = child.nextSibling;
+ if (sibling._placesNode && !aDelay) {
+ aPopup.removeChild(sibling);
+ }
+ else if (sibling._placesNode && aDelay) {
+ // HACK (bug 733419): the popups originating from the OS X native
+ // menubar don't live-update while open, thus we don't clean it
+ // until the next popupshowing, to avoid zombie menuitems.
+ if (!aPopup._delayedRemovals)
+ aPopup._delayedRemovals = [];
+ aPopup._delayedRemovals.push(sibling);
+ child = child.nextSibling;
+ }
+ else {
+ child = child.nextSibling;
+ }
+ }
+ },
+
+ _rebuildPopup: function PVB__rebuildPopup(aPopup) {
+ let resultNode = aPopup._placesNode;
+ if (!resultNode.containerOpen)
+ return;
+
+ if (this.controller.hasCachedLivemarkInfo(resultNode)) {
+ this._setEmptyPopupStatus(aPopup, false);
+ aPopup._built = true;
+ this._populateLivemarkPopup(aPopup);
+ return;
+ }
+
+ this._cleanPopup(aPopup);
+
+ let cc = resultNode.childCount;
+ if (cc > 0) {
+ this._setEmptyPopupStatus(aPopup, false);
+
+ for (let i = 0; i < cc; ++i) {
+ let child = resultNode.getChild(i);
+ this._insertNewItemToPopup(child, aPopup, null);
+ }
+ }
+ else {
+ this._setEmptyPopupStatus(aPopup, true);
+ }
+ aPopup._built = true;
+ },
+
+ _removeChild: function PVB__removeChild(aChild) {
+ // If document.popupNode pointed to this child, null it out,
+ // otherwise controller's command-updating may rely on the removed
+ // item still being "selected".
+ if (document.popupNode == aChild)
+ document.popupNode = null;
+
+ aChild.parentNode.removeChild(aChild);
+ },
+
+ _setEmptyPopupStatus:
+ function PVB__setEmptyPopupStatus(aPopup, aEmpty) {
+ if (!aPopup._emptyMenuitem) {
+ let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
+ aPopup._emptyMenuitem = document.createElement("menuitem");
+ aPopup._emptyMenuitem.setAttribute("label", label);
+ aPopup._emptyMenuitem.setAttribute("disabled", true);
+ aPopup._emptyMenuitem.className = "bookmark-item";
+ if (typeof this.options.extraClasses.entry == "string")
+ aPopup._emptyMenuitem.classList.add(this.options.extraClasses.entry);
+ }
+
+ if (aEmpty) {
+ aPopup.setAttribute("emptyplacesresult", "true");
+ // Don't add the menuitem if there is static content.
+ if (!aPopup._startMarker.previousSibling &&
+ !aPopup._endMarker.nextSibling)
+ aPopup.insertBefore(aPopup._emptyMenuitem, aPopup._endMarker);
+ }
+ else {
+ aPopup.removeAttribute("emptyplacesresult");
+ try {
+ aPopup.removeChild(aPopup._emptyMenuitem);
+ } catch (ex) {}
+ }
+ },
+
+ _createMenuItemForPlacesNode:
+ function PVB__createMenuItemForPlacesNode(aPlacesNode) {
+ this._domNodes.delete(aPlacesNode);
+
+ let element;
+ let type = aPlacesNode.type;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ element = document.createElement("menuseparator");
+ element.setAttribute("class", "small-separator");
+ }
+ else {
+ let itemId = aPlacesNode.itemId;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
+ element = document.createElement("menuitem");
+ element.className = "menuitem-iconic bookmark-item menuitem-with-favicon";
+ element.setAttribute("scheme",
+ PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri));
+ }
+ else if (PlacesUtils.containerTypes.includes(type)) {
+ element = document.createElement("menu");
+ element.setAttribute("container", "true");
+
+ if (aPlacesNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
+ element.setAttribute("query", "true");
+ if (PlacesUtils.nodeIsTagQuery(aPlacesNode))
+ element.setAttribute("tagContainer", "true");
+ else if (PlacesUtils.nodeIsDay(aPlacesNode))
+ element.setAttribute("dayContainer", "true");
+ else if (PlacesUtils.nodeIsHost(aPlacesNode))
+ element.setAttribute("hostContainer", "true");
+ }
+ else if (itemId != -1) {
+ PlacesUtils.livemarks.getLivemark({ id: itemId })
+ .then(aLivemark => {
+ element.setAttribute("livemark", "true");
+ if (AppConstants.platform === "macosx") {
+ // OS X native menubar doesn't track list-style-images since
+ // it doesn't have a frame (bug 733415). Thus enforce updating.
+ element.setAttribute("image", "");
+ element.removeAttribute("image");
+ }
+ this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+ }, () => undefined);
+ }
+
+ let popup = document.createElement("menupopup");
+ popup._placesNode = PlacesUtils.asContainer(aPlacesNode);
+
+ if (!this._nativeView) {
+ popup.setAttribute("placespopup", "true");
+ }
+
+ element.appendChild(popup);
+ element.className = "menu-iconic bookmark-item";
+ if (typeof this.options.extraClasses.entry == "string") {
+ element.classList.add(this.options.extraClasses.entry);
+ }
+
+ this._domNodes.set(aPlacesNode, popup);
+ }
+ else
+ throw "Unexpected node";
+
+ element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
+
+ let icon = aPlacesNode.icon;
+ if (icon)
+ element.setAttribute("image", icon);
+ }
+
+ element._placesNode = aPlacesNode;
+ if (!this._domNodes.has(aPlacesNode))
+ this._domNodes.set(aPlacesNode, element);
+
+ return element;
+ },
+
+ _insertNewItemToPopup:
+ function PVB__insertNewItemToPopup(aNewChild, aPopup, aBefore) {
+ let element = this._createMenuItemForPlacesNode(aNewChild);
+ let before = aBefore || aPopup._endMarker;
+
+ if (element.localName == "menuitem" || element.localName == "menu") {
+ if (typeof this.options.extraClasses.entry == "string")
+ element.classList.add(this.options.extraClasses.entry);
+ }
+
+ aPopup.insertBefore(element, before);
+ return element;
+ },
+
+ _setLivemarkSiteURIMenuItem:
+ function PVB__setLivemarkSiteURIMenuItem(aPopup) {
+ let livemarkInfo = this.controller.getCachedLivemarkInfo(aPopup._placesNode);
+ let siteUrl = livemarkInfo && livemarkInfo.siteURI ?
+ livemarkInfo.siteURI.spec : null;
+ if (!siteUrl && aPopup._siteURIMenuitem) {
+ aPopup.removeChild(aPopup._siteURIMenuitem);
+ aPopup._siteURIMenuitem = null;
+ aPopup.removeChild(aPopup._siteURIMenuseparator);
+ aPopup._siteURIMenuseparator = null;
+ }
+ else if (siteUrl && !aPopup._siteURIMenuitem) {
+ // Add "Open (Feed Name)" menuitem.
+ aPopup._siteURIMenuitem = document.createElement("menuitem");
+ aPopup._siteURIMenuitem.className = "openlivemarksite-menuitem";
+ if (typeof this.options.extraClasses.entry == "string") {
+ aPopup._siteURIMenuitem.classList.add(this.options.extraClasses.entry);
+ }
+ aPopup._siteURIMenuitem.setAttribute("targetURI", siteUrl);
+ aPopup._siteURIMenuitem.setAttribute("oncommand",
+ "openUILink(this.getAttribute('targetURI'), event);");
+
+ // If a user middle-clicks this item we serve the oncommand event.
+ // We are using checkForMiddleClick because of Bug 246720.
+ // Note: stopPropagation is needed to avoid serving middle-click
+ // with BT_onClick that would open all items in tabs.
+ aPopup._siteURIMenuitem.setAttribute("onclick",
+ "checkForMiddleClick(this, event); event.stopPropagation();");
+ let label =
+ PlacesUIUtils.getFormattedString("menuOpenLivemarkOrigin.label",
+ [aPopup.parentNode.getAttribute("label")])
+ aPopup._siteURIMenuitem.setAttribute("label", label);
+ aPopup.insertBefore(aPopup._siteURIMenuitem, aPopup._startMarker);
+
+ aPopup._siteURIMenuseparator = document.createElement("menuseparator");
+ aPopup.insertBefore(aPopup._siteURIMenuseparator, aPopup._startMarker);
+ }
+ },
+
+ /**
+ * Add, update or remove the livemark status menuitem.
+ * @param aPopup
+ * The livemark container popup
+ * @param aStatus
+ * The livemark status
+ */
+ _setLivemarkStatusMenuItem:
+ function PVB_setLivemarkStatusMenuItem(aPopup, aStatus) {
+ let statusMenuitem = aPopup._statusMenuitem;
+ if (!statusMenuitem) {
+ // Create the status menuitem and cache it in the popup object.
+ statusMenuitem = document.createElement("menuitem");
+ statusMenuitem.className = "livemarkstatus-menuitem";
+ if (typeof this.options.extraClasses.entry == "string") {
+ statusMenuitem.classList.add(this.options.extraClasses.entry);
+ }
+ statusMenuitem.setAttribute("disabled", true);
+ aPopup._statusMenuitem = statusMenuitem;
+ }
+
+ if (aStatus == Ci.mozILivemark.STATUS_LOADING ||
+ aStatus == Ci.mozILivemark.STATUS_FAILED) {
+ // Status has changed, update the cached status menuitem.
+ let stringId = aStatus == Ci.mozILivemark.STATUS_LOADING ?
+ "bookmarksLivemarkLoading" : "bookmarksLivemarkFailed";
+ statusMenuitem.setAttribute("label", PlacesUIUtils.getString(stringId));
+ if (aPopup._startMarker.nextSibling != statusMenuitem)
+ aPopup.insertBefore(statusMenuitem, aPopup._startMarker.nextSibling);
+ }
+ else if (aPopup._statusMenuitem.parentNode == aPopup) {
+ // The livemark has finished loading.
+ aPopup.removeChild(aPopup._statusMenuitem);
+ }
+ },
+
+ toggleCutNode: function PVB_toggleCutNode(aPlacesNode, aValue) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // We may get the popup for menus, but we need the menu itself.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+ if (aValue)
+ elt.setAttribute("cutting", "true");
+ else
+ elt.removeAttribute("cutting");
+ },
+
+ nodeURIChanged: function PVB_nodeURIChanged(aPlacesNode, aURIString) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ elt.setAttribute("scheme", PlacesUIUtils.guessUrlSchemeForUI(aURIString));
+ },
+
+ nodeIconChanged: function PVB_nodeIconChanged(aPlacesNode) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // There's no UI representation for the root node, thus there's nothing to
+ // be done when the icon changes.
+ if (elt == this._rootElt)
+ return;
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ let icon = aPlacesNode.icon;
+ if (!icon)
+ elt.removeAttribute("image");
+ else if (icon != elt.getAttribute("image"))
+ elt.setAttribute("image", icon);
+ },
+
+ nodeAnnotationChanged:
+ function PVB_nodeAnnotationChanged(aPlacesNode, aAnno) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // All livemarks have a feedURI, so use it as our indicator of a livemark
+ // being modified.
+ if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
+ let menu = elt.parentNode;
+ if (!menu.hasAttribute("livemark")) {
+ menu.setAttribute("livemark", "true");
+ if (AppConstants.platform === "macosx") {
+ // OS X native menubar doesn't track list-style-images since
+ // it doesn't have a frame (bug 733415). Thus enforce updating.
+ menu.setAttribute("image", "");
+ menu.removeAttribute("image");
+ }
+ }
+
+ PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
+ .then(aLivemark => {
+ // Controller will use this to build the meta data for the node.
+ this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+ this.invalidateContainer(aPlacesNode);
+ }, () => undefined);
+ }
+ },
+
+ nodeTitleChanged:
+ function PVB_nodeTitleChanged(aPlacesNode, aNewTitle) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // There's no UI representation for the root node, thus there's
+ // nothing to be done when the title changes.
+ if (elt == this._rootElt)
+ return;
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ if (!aNewTitle && elt.localName != "toolbarbutton") {
+ // Many users consider toolbars as shortcuts containers, so explicitly
+ // allow empty labels on toolbarbuttons. For any other element try to be
+ // smarter, guessing a title from the uri.
+ elt.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
+ }
+ else {
+ elt.setAttribute("label", aNewTitle);
+ }
+ },
+
+ nodeRemoved:
+ function PVB_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ if (parentElt._built) {
+ parentElt.removeChild(elt);
+
+ // Figure out if we need to show the "<Empty>" menu-item.
+ // TODO Bug 517701: This doesn't seem to handle the case of an empty
+ // root.
+ if (parentElt._startMarker.nextSibling == parentElt._endMarker)
+ this._setEmptyPopupStatus(parentElt, true);
+ }
+ },
+
+ nodeHistoryDetailsChanged:
+ function PVB_nodeHistoryDetailsChanged(aPlacesNode, aTime, aCount) {
+ if (aPlacesNode.parent &&
+ this.controller.hasCachedLivemarkInfo(aPlacesNode.parent)) {
+ // Find the node in the parent.
+ let popup = this._getDOMNodeForPlacesNode(aPlacesNode.parent);
+ for (let child = popup._startMarker.nextSibling;
+ child != popup._endMarker;
+ child = child.nextSibling) {
+ if (child._placesNode && child._placesNode.uri == aPlacesNode.uri) {
+ if (aCount)
+ child.setAttribute("visited", "true");
+ else
+ child.removeAttribute("visited");
+ break;
+ }
+ }
+ }
+ },
+
+ nodeTagsChanged: function() { },
+ nodeDateAddedChanged: function() { },
+ nodeLastModifiedChanged: function() { },
+ nodeKeywordChanged: function() { },
+ sortingChanged: function() { },
+ batching: function() { },
+
+ nodeInserted:
+ function PVB_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (!parentElt._built)
+ return;
+
+ let index = Array.prototype.indexOf.call(parentElt.childNodes, parentElt._startMarker) +
+ aIndex + 1;
+ this._insertNewItemToPopup(aPlacesNode, parentElt,
+ parentElt.childNodes[index]);
+ this._setEmptyPopupStatus(parentElt, false);
+ },
+
+ nodeMoved:
+ function PBV_nodeMoved(aPlacesNode,
+ aOldParentPlacesNode, aOldIndex,
+ aNewParentPlacesNode, aNewIndex) {
+ // Note: the current implementation of moveItem does not actually
+ // use this notification when the item in question is moved from one
+ // folder to another. Instead, it calls nodeRemoved and nodeInserted
+ // for the two folders. Thus, we can assume old-parent == new-parent.
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ // If our root node is a folder, it might be moved. There's nothing
+ // we need to do in that case.
+ if (elt == this._rootElt)
+ return;
+
+ let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
+ if (parentElt._built) {
+ // Move the node.
+ parentElt.removeChild(elt);
+ let index = Array.prototype.indexOf.call(parentElt.childNodes, parentElt._startMarker) +
+ aNewIndex + 1;
+ parentElt.insertBefore(elt, parentElt.childNodes[index]);
+ }
+ },
+
+ containerStateChanged:
+ function PVB_containerStateChanged(aPlacesNode, aOldState, aNewState) {
+ if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED ||
+ aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED) {
+ this.invalidateContainer(aPlacesNode);
+
+ if (PlacesUtils.nodeIsFolder(aPlacesNode)) {
+ let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
+ if (queryOptions.excludeItems) {
+ return;
+ }
+
+ PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
+ .then(aLivemark => {
+ let shouldInvalidate =
+ !this.controller.hasCachedLivemarkInfo(aPlacesNode);
+ this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+ if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) {
+ aLivemark.registerForUpdates(aPlacesNode, this);
+ // Prioritize the current livemark.
+ aLivemark.reload();
+ PlacesUtils.livemarks.reloadLivemarks();
+ if (shouldInvalidate)
+ this.invalidateContainer(aPlacesNode);
+ }
+ else {
+ aLivemark.unregisterForUpdates(aPlacesNode);
+ }
+ }, () => undefined);
+ }
+ }
+ },
+
+ _populateLivemarkPopup: function PVB__populateLivemarkPopup(aPopup)
+ {
+ this._setLivemarkSiteURIMenuItem(aPopup);
+ // Show the loading status only if there are no entries yet.
+ if (aPopup._startMarker.nextSibling == aPopup._endMarker)
+ this._setLivemarkStatusMenuItem(aPopup, Ci.mozILivemark.STATUS_LOADING);
+
+ PlacesUtils.livemarks.getLivemark({ id: aPopup._placesNode.itemId })
+ .then(aLivemark => {
+ let placesNode = aPopup._placesNode;
+ if (!placesNode.containerOpen)
+ return;
+
+ if (aLivemark.status != Ci.mozILivemark.STATUS_LOADING)
+ this._setLivemarkStatusMenuItem(aPopup, aLivemark.status);
+ this._cleanPopup(aPopup,
+ this._nativeView && aPopup.parentNode.hasAttribute("open"));
+
+ let children = aLivemark.getNodesForContainer(placesNode);
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+ this.nodeInserted(placesNode, child, i);
+ if (child.accessCount)
+ this._getDOMNodeForPlacesNode(child).setAttribute("visited", true);
+ else
+ this._getDOMNodeForPlacesNode(child).removeAttribute("visited");
+ }
+ }, Components.utils.reportError);
+ },
+
+ invalidateContainer: function PVB_invalidateContainer(aPlacesNode) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ elt._built = false;
+
+ // If the menupopup is open we should live-update it.
+ if (elt.parentNode.open)
+ this._rebuildPopup(elt);
+ },
+
+ uninit: function PVB_uninit() {
+ if (this._result) {
+ this._result.removeObserver(this);
+ this._resultNode.containerOpen = false;
+ this._resultNode = null;
+ this._result = null;
+ }
+
+ if (this._controller) {
+ this._controller.terminate();
+ // Removing the controller will fail if it is already no longer there.
+ // This can happen if the view element was removed/reinserted without
+ // our knowledge. There is no way to check for that having happened
+ // without the possibility of an exception. :-(
+ try {
+ this._viewElt.controllers.removeController(this._controller);
+ } catch (ex) {
+ } finally {
+ this._controller = null;
+ }
+ }
+
+ delete this._viewElt._placesView;
+ },
+
+ get isRTL() {
+ if ("_isRTL" in this)
+ return this._isRTL;
+
+ return this._isRTL = document.defaultView
+ .getComputedStyle(this.viewElt, "")
+ .direction == "rtl";
+ },
+
+ get ownerWindow() {
+ return window;
+ },
+
+ /**
+ * Adds an "Open All in Tabs" menuitem to the bottom of the popup.
+ * @param aPopup
+ * a Places popup.
+ */
+ _mayAddCommandsItems: function PVB__mayAddCommandsItems(aPopup) {
+ // The command items are never added to the root popup.
+ if (aPopup == this._rootElt)
+ return;
+
+ let hasMultipleURIs = false;
+
+ // Check if the popup contains at least 2 menuitems with places nodes.
+ // We don't currently support opening multiple uri nodes when they are not
+ // populated by the result.
+ if (aPopup._placesNode.childCount > 0) {
+ let currentChild = aPopup.firstChild;
+ let numURINodes = 0;
+ while (currentChild) {
+ if (currentChild.localName == "menuitem" && currentChild._placesNode) {
+ if (++numURINodes == 2)
+ break;
+ }
+ currentChild = currentChild.nextSibling;
+ }
+ hasMultipleURIs = numURINodes > 1;
+ }
+
+ let isLiveMark = false;
+ if (this.controller.hasCachedLivemarkInfo(aPopup._placesNode)) {
+ hasMultipleURIs = true;
+ isLiveMark = true;
+ }
+
+ if (!hasMultipleURIs) {
+ aPopup.setAttribute("singleitempopup", "true");
+ } else {
+ aPopup.removeAttribute("singleitempopup");
+ }
+
+ if (!hasMultipleURIs) {
+ // We don't have to show any option.
+ if (aPopup._endOptOpenAllInTabs) {
+ aPopup.removeChild(aPopup._endOptOpenAllInTabs);
+ aPopup._endOptOpenAllInTabs = null;
+
+ aPopup.removeChild(aPopup._endOptSeparator);
+ aPopup._endOptSeparator = null;
+ }
+ }
+ else if (!aPopup._endOptOpenAllInTabs) {
+ // Create a separator before options.
+ aPopup._endOptSeparator = document.createElement("menuseparator");
+ aPopup._endOptSeparator.className = "bookmarks-actions-menuseparator";
+ aPopup.appendChild(aPopup._endOptSeparator);
+
+ // Add the "Open All in Tabs" menuitem.
+ aPopup._endOptOpenAllInTabs = document.createElement("menuitem");
+ aPopup._endOptOpenAllInTabs.className = "openintabs-menuitem";
+
+ if (typeof this.options.extraClasses.entry == "string")
+ aPopup._endOptOpenAllInTabs.classList.add(this.options.extraClasses.entry);
+ if (typeof this.options.extraClasses.footer == "string")
+ aPopup._endOptOpenAllInTabs.classList.add(this.options.extraClasses.footer);
+
+ if (isLiveMark) {
+ aPopup._endOptOpenAllInTabs.setAttribute("oncommand",
+ "PlacesUIUtils.openLiveMarkNodesInTabs(this.parentNode._placesNode, event, " +
+ "PlacesUIUtils.getViewForNode(this));");
+ } else {
+ aPopup._endOptOpenAllInTabs.setAttribute("oncommand",
+ "PlacesUIUtils.openContainerNodeInTabs(this.parentNode._placesNode, event, " +
+ "PlacesUIUtils.getViewForNode(this));");
+ }
+ aPopup._endOptOpenAllInTabs.setAttribute("onclick",
+ "checkForMiddleClick(this, event); event.stopPropagation();");
+ aPopup._endOptOpenAllInTabs.setAttribute("label",
+ gNavigatorBundle.getString("menuOpenAllInTabs.label"));
+ aPopup.appendChild(aPopup._endOptOpenAllInTabs);
+ }
+ },
+
+ _ensureMarkers: function PVB__ensureMarkers(aPopup) {
+ if (aPopup._startMarker)
+ return;
+
+ // _startMarker is an hidden menuseparator that lives before places nodes.
+ aPopup._startMarker = document.createElement("menuseparator");
+ aPopup._startMarker.hidden = true;
+ aPopup.insertBefore(aPopup._startMarker, aPopup.firstChild);
+
+ // _endMarker is a DOM node that lives after places nodes, specified with
+ // the 'insertionPoint' option or will be a hidden menuseparator.
+ let node = ("insertionPoint" in this.options) ?
+ aPopup.querySelector(this.options.insertionPoint) : null;
+ if (node) {
+ aPopup._endMarker = node;
+ } else {
+ aPopup._endMarker = document.createElement("menuseparator");
+ aPopup._endMarker.hidden = true;
+ }
+ aPopup.appendChild(aPopup._endMarker);
+
+ // Move the markers to the right position.
+ let firstNonStaticNodeFound = false;
+ for (let i = 0; i < aPopup.childNodes.length; i++) {
+ let child = aPopup.childNodes[i];
+ // Menus that have static content at the end, but are initially empty,
+ // use a special "builder" attribute to figure out where to start
+ // inserting places nodes.
+ if (child.getAttribute("builder") == "end") {
+ aPopup.insertBefore(aPopup._endMarker, child);
+ break;
+ }
+
+ if (child._placesNode && !child.hasAttribute("simulated-places-node") &&
+ !firstNonStaticNodeFound) {
+ firstNonStaticNodeFound = true;
+ aPopup.insertBefore(aPopup._startMarker, child);
+ }
+ }
+ if (!firstNonStaticNodeFound) {
+ aPopup.insertBefore(aPopup._startMarker, aPopup._endMarker);
+ }
+ },
+
+ _onPopupShowing: function PVB__onPopupShowing(aEvent) {
+ // Avoid handling popupshowing of inner views.
+ let popup = aEvent.originalTarget;
+
+ this._ensureMarkers(popup);
+
+ // Remove any delayed element, see _cleanPopup for details.
+ if ("_delayedRemovals" in popup) {
+ while (popup._delayedRemovals.length > 0) {
+ popup.removeChild(popup._delayedRemovals.shift());
+ }
+ }
+
+ if (popup._placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
+ if (!popup._placesNode.containerOpen)
+ popup._placesNode.containerOpen = true;
+ if (!popup._built)
+ this._rebuildPopup(popup);
+
+ this._mayAddCommandsItems(popup);
+ }
+ },
+
+ _addEventListeners:
+ function PVB__addEventListeners(aObject, aEventNames, aCapturing) {
+ for (let i = 0; i < aEventNames.length; i++) {
+ aObject.addEventListener(aEventNames[i], this, aCapturing);
+ }
+ },
+
+ _removeEventListeners:
+ function PVB__removeEventListeners(aObject, aEventNames, aCapturing) {
+ for (let i = 0; i < aEventNames.length; i++) {
+ aObject.removeEventListener(aEventNames[i], this, aCapturing);
+ }
+ },
+};
+
+function PlacesToolbar(aPlace) {
+ let startTime = Date.now();
+ // Add some smart getters for our elements.
+ let thisView = this;
+ [
+ ["_viewElt", "PlacesToolbar"],
+ ["_rootElt", "PlacesToolbarItems"],
+ ["_dropIndicator", "PlacesToolbarDropIndicator"],
+ ["_chevron", "PlacesChevron"],
+ ["_chevronPopup", "PlacesChevronPopup"]
+ ].forEach(function (elementGlobal) {
+ let [name, id] = elementGlobal;
+ thisView.__defineGetter__(name, function () {
+ let element = document.getElementById(id);
+ if (!element)
+ return null;
+
+ delete thisView[name];
+ return thisView[name] = element;
+ });
+ });
+
+ this._viewElt._placesView = this;
+
+ this._addEventListeners(this._viewElt, this._cbEvents, false);
+ this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
+ this._addEventListeners(this._rootElt, ["overflow", "underflow"], true);
+ this._addEventListeners(window, ["resize", "unload"], false);
+
+ // If personal-bookmarks has been dragged to the tabs toolbar,
+ // we have to track addition and removals of tabs, to properly
+ // recalculate the available space for bookmarks.
+ // TODO (bug 734730): Use a performant mutation listener when available.
+ if (this._viewElt.parentNode.parentNode == document.getElementById("TabsToolbar")) {
+ this._addEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
+ }
+
+ PlacesViewBase.call(this, aPlace);
+
+ Services.telemetry.getHistogramById("FX_BOOKMARKS_TOOLBAR_INIT_MS")
+ .add(Date.now() - startTime);
+}
+
+PlacesToolbar.prototype = {
+ __proto__: PlacesViewBase.prototype,
+
+ _cbEvents: ["dragstart", "dragover", "dragexit", "dragend", "drop",
+ "mousemove", "mouseover", "mouseout"],
+
+ QueryInterface: function PT_QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIDOMEventListener) ||
+ aIID.equals(Ci.nsITimerCallback))
+ return this;
+
+ return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
+ },
+
+ uninit: function PT_uninit() {
+ this._removeEventListeners(this._viewElt, this._cbEvents, false);
+ this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
+ true);
+ this._removeEventListeners(this._rootElt, ["overflow", "underflow"], true);
+ this._removeEventListeners(window, ["resize", "unload"], false);
+ this._removeEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
+
+ if (this._chevron._placesView) {
+ this._chevron._placesView.uninit();
+ }
+
+ PlacesViewBase.prototype.uninit.apply(this, arguments);
+ },
+
+ _openedMenuButton: null,
+ _allowPopupShowing: true,
+
+ _rebuild: function PT__rebuild() {
+ // Clear out references to existing nodes, since they will be removed
+ // and re-added.
+ if (this._overFolder.elt)
+ this._clearOverFolder();
+
+ this._openedMenuButton = null;
+ while (this._rootElt.hasChildNodes()) {
+ this._rootElt.removeChild(this._rootElt.firstChild);
+ }
+
+ let cc = this._resultNode.childCount;
+ for (let i = 0; i < cc; ++i) {
+ this._insertNewItem(this._resultNode.getChild(i), null);
+ }
+
+ if (this._chevronPopup.hasAttribute("type")) {
+ // Chevron has already been initialized, but since we are forcing
+ // a rebuild of the toolbar, it has to be rebuilt.
+ // Otherwise, it will be initialized when the toolbar overflows.
+ this._chevronPopup.place = this.place;
+ }
+ },
+
+ _insertNewItem:
+ function PT__insertNewItem(aChild, aBefore) {
+ this._domNodes.delete(aChild);
+
+ let type = aChild.type;
+ let button;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ button = document.createElement("toolbarseparator");
+ }
+ else {
+ button = document.createElement("toolbarbutton");
+ button.className = "bookmark-item";
+ button.setAttribute("label", aChild.title || "");
+ let icon = aChild.icon;
+ if (icon)
+ button.setAttribute("image", icon);
+
+ if (PlacesUtils.containerTypes.includes(type)) {
+ button.setAttribute("type", "menu");
+ button.setAttribute("container", "true");
+
+ if (PlacesUtils.nodeIsQuery(aChild)) {
+ button.setAttribute("query", "true");
+ if (PlacesUtils.nodeIsTagQuery(aChild))
+ button.setAttribute("tagContainer", "true");
+ }
+ else if (PlacesUtils.nodeIsFolder(aChild)) {
+ PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
+ .then(aLivemark => {
+ button.setAttribute("livemark", "true");
+ this.controller.cacheLivemarkInfo(aChild, aLivemark);
+ }, () => undefined);
+ }
+
+ let popup = document.createElement("menupopup");
+ popup.setAttribute("placespopup", "true");
+ button.appendChild(popup);
+ popup._placesNode = PlacesUtils.asContainer(aChild);
+ popup.setAttribute("context", "placesContext");
+
+ this._domNodes.set(aChild, popup);
+ }
+ else if (PlacesUtils.nodeIsURI(aChild)) {
+ button.setAttribute("scheme",
+ PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
+ }
+ }
+
+ button._placesNode = aChild;
+ if (!this._domNodes.has(aChild))
+ this._domNodes.set(aChild, button);
+
+ if (aBefore)
+ this._rootElt.insertBefore(button, aBefore);
+ else
+ this._rootElt.appendChild(button);
+ },
+
+ _updateChevronPopupNodesVisibility:
+ function PT__updateChevronPopupNodesVisibility() {
+ for (let i = 0, node = this._chevronPopup._startMarker.nextSibling;
+ node != this._chevronPopup._endMarker;
+ i++, node = node.nextSibling) {
+ node.hidden = this._rootElt.childNodes[i].style.visibility != "hidden";
+ }
+ },
+
+ _onChevronPopupShowing:
+ function PT__onChevronPopupShowing(aEvent) {
+ // Handle popupshowing only for the chevron popup, not for nested ones.
+ if (aEvent.target != this._chevronPopup)
+ return;
+
+ if (!this._chevron._placesView)
+ this._chevron._placesView = new PlacesMenu(aEvent, this.place);
+
+ this._updateChevronPopupNodesVisibility();
+ },
+
+ handleEvent: function PT_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "unload":
+ this.uninit();
+ break;
+ case "resize":
+ // This handler updates nodes visibility in both the toolbar
+ // and the chevron popup when a window resize does not change
+ // the overflow status of the toolbar.
+ this.updateChevron();
+ break;
+ case "overflow":
+ if (!this._isOverflowStateEventRelevant(aEvent))
+ return;
+ this._onOverflow();
+ break;
+ case "underflow":
+ if (!this._isOverflowStateEventRelevant(aEvent))
+ return;
+ this._onUnderflow();
+ break;
+ case "TabOpen":
+ case "TabClose":
+ this.updateChevron();
+ break;
+ case "dragstart":
+ this._onDragStart(aEvent);
+ break;
+ case "dragover":
+ this._onDragOver(aEvent);
+ break;
+ case "dragexit":
+ this._onDragExit(aEvent);
+ break;
+ case "dragend":
+ this._onDragEnd(aEvent);
+ break;
+ case "drop":
+ this._onDrop(aEvent);
+ break;
+ case "mouseover":
+ this._onMouseOver(aEvent);
+ break;
+ case "mousemove":
+ this._onMouseMove(aEvent);
+ break;
+ case "mouseout":
+ this._onMouseOut(aEvent);
+ break;
+ case "popupshowing":
+ this._onPopupShowing(aEvent);
+ break;
+ case "popuphidden":
+ this._onPopupHidden(aEvent);
+ break;
+ default:
+ throw "Trying to handle unexpected event.";
+ }
+ },
+
+ updateOverflowStatus: function() {
+ if (this._rootElt.scrollLeftMin != this._rootElt.scrollLeftMax) {
+ this._onOverflow();
+ } else {
+ this._onUnderflow();
+ }
+ },
+
+ _isOverflowStateEventRelevant: function PT_isOverflowStateEventRelevant(aEvent) {
+ // Ignore events not aimed at ourselves, as well as purely vertical ones:
+ return aEvent.target == aEvent.currentTarget && aEvent.detail > 0;
+ },
+
+ _onOverflow: function PT_onOverflow() {
+ // Attach the popup binding to the chevron popup if it has not yet
+ // been initialized.
+ if (!this._chevronPopup.hasAttribute("type")) {
+ this._chevronPopup.setAttribute("place", this.place);
+ this._chevronPopup.setAttribute("type", "places");
+ }
+ this._chevron.collapsed = false;
+ this.updateChevron();
+ },
+
+ _onUnderflow: function PT_onUnderflow() {
+ this.updateChevron();
+ this._chevron.collapsed = true;
+ },
+
+ updateChevron: function PT_updateChevron() {
+ // If the chevron is collapsed there's nothing to update.
+ if (this._chevron.collapsed)
+ return;
+
+ // Update the chevron on a timer. This will avoid repeated work when
+ // lot of changes happen in a small timeframe.
+ if (this._updateChevronTimer)
+ this._updateChevronTimer.cancel();
+
+ this._updateChevronTimer = this._setTimer(100);
+ },
+
+ _updateChevronTimerCallback: function PT__updateChevronTimerCallback() {
+ let scrollRect = this._rootElt.getBoundingClientRect();
+ let childOverflowed = false;
+ for (let i = 0; i < this._rootElt.childNodes.length; i++) {
+ let child = this._rootElt.childNodes[i];
+ // Once a child overflows, all the next ones will.
+ if (!childOverflowed) {
+ let childRect = child.getBoundingClientRect();
+ childOverflowed = this.isRTL ? (childRect.left < scrollRect.left)
+ : (childRect.right > scrollRect.right);
+
+ }
+ child.style.visibility = childOverflowed ? "hidden" : "visible";
+ }
+
+ // We rebuild the chevron on popupShowing, so if it is open
+ // we must update it.
+ if (this._chevron.open)
+ this._updateChevronPopupNodesVisibility();
+ },
+
+ nodeInserted:
+ function PT_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (parentElt == this._rootElt) {
+ let children = this._rootElt.childNodes;
+ this._insertNewItem(aPlacesNode,
+ aIndex < children.length ? children[aIndex] : null);
+ this.updateChevron();
+ return;
+ }
+
+ PlacesViewBase.prototype.nodeInserted.apply(this, arguments);
+ },
+
+ nodeRemoved:
+ function PT_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ if (parentElt == this._rootElt) {
+ this._removeChild(elt);
+ this.updateChevron();
+ return;
+ }
+
+ PlacesViewBase.prototype.nodeRemoved.apply(this, arguments);
+ },
+
+ nodeMoved:
+ function PT_nodeMoved(aPlacesNode,
+ aOldParentPlacesNode, aOldIndex,
+ aNewParentPlacesNode, aNewIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
+ if (parentElt == this._rootElt) {
+ // Container is on the toolbar.
+
+ // Move the element.
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ this._removeChild(elt);
+ this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]);
+
+ // The chevron view may get nodeMoved after the toolbar. In such a case,
+ // we should ensure (by manually swapping menuitems) that the actual nodes
+ // are in the final position before updateChevron tries to updates their
+ // visibility, or the chevron may go out of sync.
+ // Luckily updateChevron runs on a timer, so, by the time it updates
+ // nodes, the menu has already handled the notification.
+
+ this.updateChevron();
+ return;
+ }
+
+ PlacesViewBase.prototype.nodeMoved.apply(this, arguments);
+ },
+
+ nodeAnnotationChanged:
+ function PT_nodeAnnotationChanged(aPlacesNode, aAnno) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ if (elt == this._rootElt)
+ return;
+
+ // We're notified for the menupopup, not the containing toolbarbutton.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ if (elt.parentNode == this._rootElt) {
+ // Node is on the toolbar.
+
+ // All livemarks have a feedURI, so use it as our indicator.
+ if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
+ elt.setAttribute("livemark", true);
+
+ PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
+ .then(aLivemark => {
+ this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+ this.invalidateContainer(aPlacesNode);
+ }, Components.utils.reportError);
+ }
+ }
+ else {
+ // Node is in a submenu.
+ PlacesViewBase.prototype.nodeAnnotationChanged.apply(this, arguments);
+ }
+ },
+
+ nodeTitleChanged: function PT_nodeTitleChanged(aPlacesNode, aNewTitle) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // There's no UI representation for the root node, thus there's
+ // nothing to be done when the title changes.
+ if (elt == this._rootElt)
+ return;
+
+ PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ if (elt.parentNode == this._rootElt) {
+ // Node is on the toolbar
+ this.updateChevron();
+ }
+ },
+
+ invalidateContainer: function PT_invalidateContainer(aPlacesNode) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ if (elt == this._rootElt) {
+ // Container is the toolbar itself.
+ this._rebuild();
+ return;
+ }
+
+ PlacesViewBase.prototype.invalidateContainer.apply(this, arguments);
+ },
+
+ _overFolder: { elt: null,
+ openTimer: null,
+ hoverTime: 350,
+ closeTimer: null },
+
+ _clearOverFolder: function PT__clearOverFolder() {
+ // The mouse is no longer dragging over the stored menubutton.
+ // Close the menubutton, clear out drag styles, and clear all
+ // timers for opening/closing it.
+ if (this._overFolder.elt && this._overFolder.elt.lastChild) {
+ if (!this._overFolder.elt.lastChild.hasAttribute("dragover")) {
+ this._overFolder.elt.lastChild.hidePopup();
+ }
+ this._overFolder.elt.removeAttribute("dragover");
+ this._overFolder.elt = null;
+ }
+ if (this._overFolder.openTimer) {
+ this._overFolder.openTimer.cancel();
+ this._overFolder.openTimer = null;
+ }
+ if (this._overFolder.closeTimer) {
+ this._overFolder.closeTimer.cancel();
+ this._overFolder.closeTimer = null;
+ }
+ },
+
+ /**
+ * This function returns information about where to drop when dragging over
+ * the toolbar. The returned object has the following properties:
+ * - ip: the insertion point for the bookmarks service.
+ * - beforeIndex: child index to drop before, for the drop indicator.
+ * - folderElt: the folder to drop into, if applicable.
+ */
+ _getDropPoint: function PT__getDropPoint(aEvent) {
+ if (!PlacesUtils.nodeIsFolder(this._resultNode))
+ return null;
+
+ let dropPoint = { ip: null, beforeIndex: null, folderElt: null };
+ let elt = aEvent.target;
+ if (elt._placesNode && elt != this._rootElt &&
+ elt.localName != "menupopup") {
+ let eltRect = elt.getBoundingClientRect();
+ let eltIndex = Array.prototype.indexOf.call(this._rootElt.childNodes, elt);
+ if (PlacesUtils.nodeIsFolder(elt._placesNode) &&
+ !PlacesUIUtils.isContentsReadOnly(elt._placesNode)) {
+ // This is a folder.
+ // If we are in the middle of it, drop inside it.
+ // Otherwise, drop before it, with regards to RTL mode.
+ let threshold = eltRect.width * 0.25;
+ if (this.isRTL ? (aEvent.clientX > eltRect.right - threshold)
+ : (aEvent.clientX < eltRect.left + threshold)) {
+ // Drop before this folder.
+ dropPoint.ip =
+ new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
+ eltIndex, Ci.nsITreeView.DROP_BEFORE);
+ dropPoint.beforeIndex = eltIndex;
+ }
+ else if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
+ : (aEvent.clientX < eltRect.right - threshold)) {
+ // Drop inside this folder.
+ let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) ?
+ elt._placesNode.title : null;
+ dropPoint.ip =
+ new InsertionPoint(PlacesUtils.getConcreteItemId(elt._placesNode),
+ -1, Ci.nsITreeView.DROP_ON,
+ tagName);
+ dropPoint.beforeIndex = eltIndex;
+ dropPoint.folderElt = elt;
+ }
+ else {
+ // Drop after this folder.
+ let beforeIndex =
+ (eltIndex == this._rootElt.childNodes.length - 1) ?
+ -1 : eltIndex + 1;
+
+ dropPoint.ip =
+ new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
+ beforeIndex, Ci.nsITreeView.DROP_BEFORE);
+ dropPoint.beforeIndex = beforeIndex;
+ }
+ }
+ else {
+ // This is a non-folder node or a read-only folder.
+ // Drop before it with regards to RTL mode.
+ let threshold = eltRect.width * 0.5;
+ if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
+ : (aEvent.clientX < eltRect.left + threshold)) {
+ // Drop before this bookmark.
+ dropPoint.ip =
+ new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
+ eltIndex, Ci.nsITreeView.DROP_BEFORE);
+ dropPoint.beforeIndex = eltIndex;
+ }
+ else {
+ // Drop after this bookmark.
+ let beforeIndex =
+ eltIndex == this._rootElt.childNodes.length - 1 ?
+ -1 : eltIndex + 1;
+ dropPoint.ip =
+ new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
+ beforeIndex, Ci.nsITreeView.DROP_BEFORE);
+ dropPoint.beforeIndex = beforeIndex;
+ }
+ }
+ }
+ else {
+ // We are most likely dragging on the empty area of the
+ // toolbar, we should drop after the last node.
+ dropPoint.ip =
+ new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
+ -1, Ci.nsITreeView.DROP_BEFORE);
+ dropPoint.beforeIndex = -1;
+ }
+
+ return dropPoint;
+ },
+
+ _setTimer: function PT_setTimer(aTime) {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT);
+ return timer;
+ },
+
+ notify: function PT_notify(aTimer) {
+ if (aTimer == this._updateChevronTimer) {
+ this._updateChevronTimer = null;
+ this._updateChevronTimerCallback();
+ }
+
+ // * Timer to turn off indicator bar.
+ else if (aTimer == this._ibTimer) {
+ this._dropIndicator.collapsed = true;
+ this._ibTimer = null;
+ }
+
+ // * Timer to open a menubutton that's being dragged over.
+ else if (aTimer == this._overFolder.openTimer) {
+ // Set the autoopen attribute on the folder's menupopup so that
+ // the menu will automatically close when the mouse drags off of it.
+ this._overFolder.elt.lastChild.setAttribute("autoopened", "true");
+ this._overFolder.elt.open = true;
+ this._overFolder.openTimer = null;
+ }
+
+ // * Timer to close a menubutton that's been dragged off of.
+ else if (aTimer == this._overFolder.closeTimer) {
+ // Close the menubutton if we are not dragging over it or one of
+ // its children. The autoopened attribute will let the menu know to
+ // close later if the menu is still being dragged over.
+ let currentPlacesNode = PlacesControllerDragHelper.currentDropTarget;
+ let inHierarchy = false;
+ while (currentPlacesNode) {
+ if (currentPlacesNode == this._rootElt) {
+ inHierarchy = true;
+ break;
+ }
+ currentPlacesNode = currentPlacesNode.parentNode;
+ }
+ // The _clearOverFolder() function will close the menu for
+ // _overFolder.elt. So null it out if we don't want to close it.
+ if (inHierarchy)
+ this._overFolder.elt = null;
+
+ // Clear out the folder and all associated timers.
+ this._clearOverFolder();
+ }
+ },
+
+ _onMouseOver: function PT__onMouseOver(aEvent) {
+ let button = aEvent.target;
+ if (button.parentNode == this._rootElt && button._placesNode &&
+ PlacesUtils.nodeIsURI(button._placesNode))
+ window.XULBrowserWindow.setOverLink(aEvent.target._placesNode.uri, null);
+ },
+
+ _onMouseOut: function PT__onMouseOut(aEvent) {
+ window.XULBrowserWindow.setOverLink("", null);
+ },
+
+ _cleanupDragDetails: function PT__cleanupDragDetails() {
+ // Called on dragend and drop.
+ PlacesControllerDragHelper.currentDropTarget = null;
+ this._draggedElt = null;
+ if (this._ibTimer)
+ this._ibTimer.cancel();
+
+ this._dropIndicator.collapsed = true;
+ },
+
+ _onDragStart: function PT__onDragStart(aEvent) {
+ // Sub menus have their own d&d handlers.
+ let draggedElt = aEvent.target;
+ if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode)
+ return;
+
+ if (draggedElt.localName == "toolbarbutton" &&
+ draggedElt.getAttribute("type") == "menu") {
+ // If the drag gesture on a container is toward down we open instead
+ // of dragging.
+ let translateY = this._cachedMouseMoveEvent.clientY - aEvent.clientY;
+ let translateX = this._cachedMouseMoveEvent.clientX - aEvent.clientX;
+ if ((translateY) >= Math.abs(translateX/2)) {
+ // Don't start the drag.
+ aEvent.preventDefault();
+ // Open the menu.
+ draggedElt.open = true;
+ return;
+ }
+
+ // If the menu is open, close it.
+ if (draggedElt.open) {
+ draggedElt.lastChild.hidePopup();
+ draggedElt.open = false;
+ }
+ }
+
+ // Activate the view and cache the dragged element.
+ this._draggedElt = draggedElt._placesNode;
+ this._rootElt.focus();
+
+ this._controller.setDataTransfer(aEvent);
+ aEvent.stopPropagation();
+ },
+
+ _onDragOver: function PT__onDragOver(aEvent) {
+ // Cache the dataTransfer
+ PlacesControllerDragHelper.currentDropTarget = aEvent.target;
+ let dt = aEvent.dataTransfer;
+
+ let dropPoint = this._getDropPoint(aEvent);
+ if (!dropPoint || !dropPoint.ip ||
+ !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)) {
+ this._dropIndicator.collapsed = true;
+ aEvent.stopPropagation();
+ return;
+ }
+
+ if (this._ibTimer) {
+ this._ibTimer.cancel();
+ this._ibTimer = null;
+ }
+
+ if (dropPoint.folderElt || aEvent.originalTarget == this._chevron) {
+ // Dropping over a menubutton or chevron button.
+ // Set styles and timer to open relative menupopup.
+ let overElt = dropPoint.folderElt || this._chevron;
+ if (this._overFolder.elt != overElt) {
+ this._clearOverFolder();
+ this._overFolder.elt = overElt;
+ this._overFolder.openTimer = this._setTimer(this._overFolder.hoverTime);
+ }
+ if (!this._overFolder.elt.hasAttribute("dragover"))
+ this._overFolder.elt.setAttribute("dragover", "true");
+
+ this._dropIndicator.collapsed = true;
+ }
+ else {
+ // Dragging over a normal toolbarbutton,
+ // show indicator bar and move it to the appropriate drop point.
+ let ind = this._dropIndicator;
+ ind.parentNode.collapsed = false;
+ let halfInd = ind.clientWidth / 2;
+ let translateX;
+ if (this.isRTL) {
+ halfInd = Math.ceil(halfInd);
+ translateX = 0 - this._rootElt.getBoundingClientRect().right - halfInd;
+ if (this._rootElt.firstChild) {
+ if (dropPoint.beforeIndex == -1)
+ translateX += this._rootElt.lastChild.getBoundingClientRect().left;
+ else {
+ translateX += this._rootElt.childNodes[dropPoint.beforeIndex]
+ .getBoundingClientRect().right;
+ }
+ }
+ }
+ else {
+ halfInd = Math.floor(halfInd);
+ translateX = 0 - this._rootElt.getBoundingClientRect().left +
+ halfInd;
+ if (this._rootElt.firstChild) {
+ if (dropPoint.beforeIndex == -1)
+ translateX += this._rootElt.lastChild.getBoundingClientRect().right;
+ else {
+ translateX += this._rootElt.childNodes[dropPoint.beforeIndex]
+ .getBoundingClientRect().left;
+ }
+ }
+ }
+
+ ind.style.transform = "translate(" + Math.round(translateX) + "px)";
+ ind.style.marginInlineStart = (-ind.clientWidth) + "px";
+ ind.collapsed = false;
+
+ // Clear out old folder information.
+ this._clearOverFolder();
+ }
+
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ },
+
+ _onDrop: function PT__onDrop(aEvent) {
+ PlacesControllerDragHelper.currentDropTarget = aEvent.target;
+
+ let dropPoint = this._getDropPoint(aEvent);
+ if (dropPoint && dropPoint.ip) {
+ PlacesControllerDragHelper.onDrop(dropPoint.ip, aEvent.dataTransfer)
+ .then(null, Components.utils.reportError);
+ aEvent.preventDefault();
+ }
+
+ this._cleanupDragDetails();
+ aEvent.stopPropagation();
+ },
+
+ _onDragExit: function PT__onDragExit(aEvent) {
+ PlacesControllerDragHelper.currentDropTarget = null;
+
+ // Set timer to turn off indicator bar (if we turn it off
+ // here, dragenter might be called immediately after, creating
+ // flicker).
+ if (this._ibTimer)
+ this._ibTimer.cancel();
+ this._ibTimer = this._setTimer(10);
+
+ // If we hovered over a folder, close it now.
+ if (this._overFolder.elt)
+ this._overFolder.closeTimer = this._setTimer(this._overFolder.hoverTime);
+ },
+
+ _onDragEnd: function PT_onDragEnd(aEvent) {
+ this._cleanupDragDetails();
+ },
+
+ _onPopupShowing: function PT__onPopupShowing(aEvent) {
+ if (!this._allowPopupShowing) {
+ this._allowPopupShowing = true;
+ aEvent.preventDefault();
+ return;
+ }
+
+ let parent = aEvent.target.parentNode;
+ if (parent.localName == "toolbarbutton")
+ this._openedMenuButton = parent;
+
+ PlacesViewBase.prototype._onPopupShowing.apply(this, arguments);
+ },
+
+ _onPopupHidden: function PT__onPopupHidden(aEvent) {
+ let popup = aEvent.target;
+ let placesNode = popup._placesNode;
+ // Avoid handling popuphidden of inner views
+ if (placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
+ // UI performance: folder queries are cheap, keep the resultnode open
+ // so we don't rebuild its contents whenever the popup is reopened.
+ // Though, we want to always close feed containers so their expiration
+ // status will be checked at next opening.
+ if (!PlacesUtils.nodeIsFolder(placesNode) ||
+ this.controller.hasCachedLivemarkInfo(placesNode)) {
+ placesNode.containerOpen = false;
+ }
+ }
+
+ let parent = popup.parentNode;
+ if (parent.localName == "toolbarbutton") {
+ this._openedMenuButton = null;
+ // Clear the dragover attribute if present, if we are dragging into a
+ // folder in the hierachy of current opened popup we don't clear
+ // this attribute on clearOverFolder. See Notify for closeTimer.
+ if (parent.hasAttribute("dragover"))
+ parent.removeAttribute("dragover");
+ }
+ },
+
+ _onMouseMove: function PT__onMouseMove(aEvent) {
+ // Used in dragStart to prevent dragging folders when dragging down.
+ this._cachedMouseMoveEvent = aEvent;
+
+ if (this._openedMenuButton == null ||
+ PlacesControllerDragHelper.getSession())
+ return;
+
+ let target = aEvent.originalTarget;
+ if (this._openedMenuButton != target &&
+ target.localName == "toolbarbutton" &&
+ target.type == "menu") {
+ this._openedMenuButton.open = false;
+ target.open = true;
+ }
+ }
+};
+
+/**
+ * View for Places menus. This object should be created during the first
+ * popupshowing that's dispatched on the menu.
+ */
+function PlacesMenu(aPopupShowingEvent, aPlace, aOptions) {
+ this._rootElt = aPopupShowingEvent.target; // <menupopup>
+ this._viewElt = this._rootElt.parentNode; // <menu>
+ this._viewElt._placesView = this;
+ this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
+ this._addEventListeners(window, ["unload"], false);
+
+ if (AppConstants.platform === "macosx") {
+ // Must walk up to support views in sub-menus, like Bookmarks Toolbar menu.
+ for (let elt = this._viewElt.parentNode; elt; elt = elt.parentNode) {
+ if (elt.localName == "menubar") {
+ this._nativeView = true;
+ break;
+ }
+ }
+ }
+
+ PlacesViewBase.call(this, aPlace, aOptions);
+ this._onPopupShowing(aPopupShowingEvent);
+}
+
+PlacesMenu.prototype = {
+ __proto__: PlacesViewBase.prototype,
+
+ QueryInterface: function PM_QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIDOMEventListener))
+ return this;
+
+ return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
+ },
+
+ _removeChild: function PM_removeChild(aChild) {
+ PlacesViewBase.prototype._removeChild.apply(this, arguments);
+ },
+
+ uninit: function PM_uninit() {
+ this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
+ true);
+ this._removeEventListeners(window, ["unload"], false);
+
+ PlacesViewBase.prototype.uninit.apply(this, arguments);
+ },
+
+ handleEvent: function PM_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "unload":
+ this.uninit();
+ break;
+ case "popupshowing":
+ this._onPopupShowing(aEvent);
+ break;
+ case "popuphidden":
+ this._onPopupHidden(aEvent);
+ break;
+ }
+ },
+
+ _onPopupHidden: function PM__onPopupHidden(aEvent) {
+ // Avoid handling popuphidden of inner views.
+ let popup = aEvent.originalTarget;
+ let placesNode = popup._placesNode;
+ if (!placesNode || PlacesUIUtils.getViewForNode(popup) != this)
+ return;
+
+ // UI performance: folder queries are cheap, keep the resultnode open
+ // so we don't rebuild its contents whenever the popup is reopened.
+ // Though, we want to always close feed containers so their expiration
+ // status will be checked at next opening.
+ if (!PlacesUtils.nodeIsFolder(placesNode) ||
+ this.controller.hasCachedLivemarkInfo(placesNode))
+ placesNode.containerOpen = false;
+
+ // The autoopened attribute is set for folders which have been
+ // automatically opened when dragged over. Turn off this attribute
+ // when the folder closes because it is no longer applicable.
+ popup.removeAttribute("autoopened");
+ popup.removeAttribute("dragstart");
+ }
+};
+
+function PlacesPanelMenuView(aPlace, aViewId, aRootId, aOptions) {
+ this._viewElt = document.getElementById(aViewId);
+ this._rootElt = document.getElementById(aRootId);
+ this._viewElt._placesView = this;
+ this.options = aOptions;
+
+ PlacesViewBase.call(this, aPlace, aOptions);
+}
+
+PlacesPanelMenuView.prototype = {
+ __proto__: PlacesViewBase.prototype,
+
+ QueryInterface: function PAMV_QueryInterface(aIID) {
+ return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
+ },
+
+ uninit: function PAMV_uninit() {
+ PlacesViewBase.prototype.uninit.apply(this, arguments);
+ },
+
+ _insertNewItem:
+ function PAMV__insertNewItem(aChild, aBefore) {
+ this._domNodes.delete(aChild);
+
+ let type = aChild.type;
+ let button;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ button = document.createElement("toolbarseparator");
+ button.setAttribute("class", "small-separator");
+ }
+ else {
+ button = document.createElement("toolbarbutton");
+ button.className = "bookmark-item";
+ if (typeof this.options.extraClasses.entry == "string")
+ button.classList.add(this.options.extraClasses.entry);
+ button.setAttribute("label", aChild.title || "");
+ let icon = aChild.icon;
+ if (icon)
+ button.setAttribute("image", icon);
+
+ if (PlacesUtils.containerTypes.includes(type)) {
+ button.setAttribute("container", "true");
+
+ if (PlacesUtils.nodeIsQuery(aChild)) {
+ button.setAttribute("query", "true");
+ if (PlacesUtils.nodeIsTagQuery(aChild))
+ button.setAttribute("tagContainer", "true");
+ }
+ else if (PlacesUtils.nodeIsFolder(aChild)) {
+ PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
+ .then(aLivemark => {
+ button.setAttribute("livemark", "true");
+ this.controller.cacheLivemarkInfo(aChild, aLivemark);
+ }, () => undefined);
+ }
+ }
+ else if (PlacesUtils.nodeIsURI(aChild)) {
+ button.setAttribute("scheme",
+ PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
+ }
+ }
+
+ button._placesNode = aChild;
+ if (!this._domNodes.has(aChild))
+ this._domNodes.set(aChild, button);
+
+ this._rootElt.insertBefore(button, aBefore);
+ },
+
+ nodeInserted:
+ function PAMV_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (parentElt != this._rootElt)
+ return;
+
+ let children = this._rootElt.childNodes;
+ this._insertNewItem(aPlacesNode,
+ aIndex < children.length ? children[aIndex] : null);
+ },
+
+ nodeRemoved:
+ function PAMV_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (parentElt != this._rootElt)
+ return;
+
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ this._removeChild(elt);
+ },
+
+ nodeMoved:
+ function PAMV_nodeMoved(aPlacesNode,
+ aOldParentPlacesNode, aOldIndex,
+ aNewParentPlacesNode, aNewIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
+ if (parentElt != this._rootElt)
+ return;
+
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ this._removeChild(elt);
+ this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]);
+ },
+
+ nodeAnnotationChanged:
+ function PAMV_nodeAnnotationChanged(aPlacesNode, aAnno) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ // There's no UI representation for the root node.
+ if (elt == this._rootElt)
+ return;
+
+ if (elt.parentNode != this._rootElt)
+ return;
+
+ // All livemarks have a feedURI, so use it as our indicator.
+ if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
+ elt.setAttribute("livemark", true);
+
+ PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
+ .then(aLivemark => {
+ this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+ this.invalidateContainer(aPlacesNode);
+ }, Components.utils.reportError);
+ }
+ },
+
+ nodeTitleChanged: function PAMV_nodeTitleChanged(aPlacesNode, aNewTitle) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // There's no UI representation for the root node.
+ if (elt == this._rootElt)
+ return;
+
+ PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
+ },
+
+ invalidateContainer: function PAMV_invalidateContainer(aPlacesNode) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ if (elt != this._rootElt)
+ return;
+
+ // Container is the toolbar itself.
+ while (this._rootElt.hasChildNodes()) {
+ this._rootElt.removeChild(this._rootElt.firstChild);
+ }
+
+ for (let i = 0; i < this._resultNode.childCount; ++i) {
+ this._insertNewItem(this._resultNode.getChild(i), null);
+ }
+ }
+};
diff --git a/browser/components/places/content/controller.js b/browser/components/places/content/controller.js
new file mode 100644
index 000000000..0d66fbcaf
--- /dev/null
+++ b/browser/components/places/content/controller.js
@@ -0,0 +1,1742 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+XPCOMUtils.defineLazyModuleGetter(this, "ForgetAboutSite",
+ "resource://gre/modules/ForgetAboutSite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+// XXXmano: we should move most/all of these constants to PlacesUtils
+const ORGANIZER_ROOT_BOOKMARKS = "place:folder=BOOKMARKS_MENU&excludeItems=1&queryType=1";
+
+// No change to the view, preserve current selection
+const RELOAD_ACTION_NOTHING = 0;
+// Inserting items new to the view, select the inserted rows
+const RELOAD_ACTION_INSERT = 1;
+// Removing items from the view, select the first item after the last selected
+const RELOAD_ACTION_REMOVE = 2;
+// Moving items within a view, don't treat the dropped items as additional
+// rows.
+const RELOAD_ACTION_MOVE = 3;
+
+// When removing a bunch of pages we split them in chunks to give some breath
+// to the main-thread.
+const REMOVE_PAGES_CHUNKLEN = 300;
+
+/**
+ * Represents an insertion point within a container where we can insert
+ * items.
+ * @param aItemId
+ * The identifier of the parent container
+ * @param aIndex
+ * The index within the container where we should insert
+ * @param aOrientation
+ * The orientation of the insertion. NOTE: the adjustments to the
+ * insertion point to accommodate the orientation should be done by
+ * the person who constructs the IP, not the user. The orientation
+ * is provided for informational purposes only!
+ * @param [optional] aTag
+ * The tag name if this IP is set to a tag, null otherwise.
+ * @param [optional] aDropNearItemId
+ * When defined we will calculate index based on this itemId
+ * @constructor
+ */
+function InsertionPoint(aItemId, aIndex, aOrientation, aTagName = null,
+ aDropNearItemId = false) {
+ this.itemId = aItemId;
+ this._index = aIndex;
+ this.orientation = aOrientation;
+ this.tagName = aTagName;
+ this.dropNearItemId = aDropNearItemId;
+}
+
+InsertionPoint.prototype = {
+ set index(val) {
+ return this._index = val;
+ },
+
+ promiseGuid: function () {
+ return PlacesUtils.promiseItemGuid(this.itemId);
+ },
+
+ get index() {
+ if (this.dropNearItemId > 0) {
+ // If dropNearItemId is set up we must calculate the real index of
+ // the item near which we will drop.
+ var index = PlacesUtils.bookmarks.getItemIndex(this.dropNearItemId);
+ return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1;
+ }
+ return this._index;
+ },
+
+ get isTag() {
+ return typeof(this.tagName) == "string";
+ }
+};
+
+/**
+ * Places Controller
+ */
+
+function PlacesController(aView) {
+ this._view = aView;
+ XPCOMUtils.defineLazyServiceGetter(this, "clipboard",
+ "@mozilla.org/widget/clipboard;1",
+ "nsIClipboard");
+ XPCOMUtils.defineLazyGetter(this, "profileName", function () {
+ return Services.dirsvc.get("ProfD", Ci.nsIFile).leafName;
+ });
+
+ this._cachedLivemarkInfoObjects = new Map();
+}
+
+PlacesController.prototype = {
+ /**
+ * The places view.
+ */
+ _view: null,
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIClipboardOwner
+ ]),
+
+ // nsIClipboardOwner
+ LosingOwnership: function PC_LosingOwnership (aXferable) {
+ this.cutNodes = [];
+ },
+
+ terminate: function PC_terminate() {
+ this._releaseClipboardOwnership();
+ },
+
+ supportsCommand: function PC_supportsCommand(aCommand) {
+ // Non-Places specific commands that we also support
+ switch (aCommand) {
+ case "cmd_undo":
+ case "cmd_redo":
+ case "cmd_cut":
+ case "cmd_copy":
+ case "cmd_paste":
+ case "cmd_delete":
+ case "cmd_selectAll":
+ return true;
+ }
+
+ // All other Places Commands are prefixed with "placesCmd_" ... this
+ // filters out other commands that we do _not_ support (see 329587).
+ const CMD_PREFIX = "placesCmd_";
+ return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
+ },
+
+ isCommandEnabled: function PC_isCommandEnabled(aCommand) {
+ switch (aCommand) {
+ case "cmd_undo":
+ if (!PlacesUIUtils.useAsyncTransactions)
+ return PlacesUtils.transactionManager.numberOfUndoItems > 0;
+
+ return PlacesTransactions.topUndoEntry != null;
+ case "cmd_redo":
+ if (!PlacesUIUtils.useAsyncTransactions)
+ return PlacesUtils.transactionManager.numberOfRedoItems > 0;
+
+ return PlacesTransactions.topRedoEntry != null;
+ case "cmd_cut":
+ case "placesCmd_cut":
+ case "placesCmd_moveBookmarks":
+ for (let node of this._view.selectedNodes) {
+ // If selection includes history nodes or tags-as-bookmark, disallow
+ // cutting.
+ if (node.itemId == -1 ||
+ (node.parent && PlacesUtils.nodeIsTagQuery(node.parent))) {
+ return false;
+ }
+ }
+ // Otherwise fall through the cmd_delete check.
+ case "cmd_delete":
+ case "placesCmd_delete":
+ case "placesCmd_deleteDataHost":
+ return this._hasRemovableSelection();
+ case "cmd_copy":
+ case "placesCmd_copy":
+ return this._view.hasSelection;
+ case "cmd_paste":
+ case "placesCmd_paste":
+ return this._canInsert(true) && this._isClipboardDataPasteable();
+ case "cmd_selectAll":
+ if (this._view.selType != "single") {
+ let rootNode = this._view.result.root;
+ if (rootNode.containerOpen && rootNode.childCount > 0)
+ return true;
+ }
+ return false;
+ case "placesCmd_open":
+ case "placesCmd_open:window":
+ case "placesCmd_open:privatewindow":
+ case "placesCmd_open:tab":
+ var selectedNode = this._view.selectedNode;
+ return selectedNode && PlacesUtils.nodeIsURI(selectedNode);
+ case "placesCmd_new:folder":
+ return this._canInsert();
+ case "placesCmd_new:bookmark":
+ return this._canInsert();
+ case "placesCmd_new:separator":
+ return this._canInsert() &&
+ !PlacesUtils.asQuery(this._view.result.root).queryOptions.excludeItems &&
+ this._view.result.sortingMode ==
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ case "placesCmd_show:info": {
+ let selectedNode = this._view.selectedNode;
+ return selectedNode && PlacesUtils.getConcreteItemId(selectedNode) != -1
+ }
+ case "placesCmd_reload": {
+ // Livemark containers
+ let selectedNode = this._view.selectedNode;
+ return selectedNode && this.hasCachedLivemarkInfo(selectedNode);
+ }
+ case "placesCmd_sortBy:name": {
+ let selectedNode = this._view.selectedNode;
+ return selectedNode &&
+ PlacesUtils.nodeIsFolder(selectedNode) &&
+ !PlacesUIUtils.isContentsReadOnly(selectedNode) &&
+ this._view.result.sortingMode ==
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ }
+ case "placesCmd_createBookmark":
+ var node = this._view.selectedNode;
+ return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1;
+ default:
+ return false;
+ }
+ },
+
+ doCommand: function PC_doCommand(aCommand) {
+ switch (aCommand) {
+ case "cmd_undo":
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ PlacesUtils.transactionManager.undoTransaction();
+ return;
+ }
+ PlacesTransactions.undo().then(null, Components.utils.reportError);
+ break;
+ case "cmd_redo":
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ PlacesUtils.transactionManager.redoTransaction();
+ return;
+ }
+ PlacesTransactions.redo().then(null, Components.utils.reportError);
+ break;
+ case "cmd_cut":
+ case "placesCmd_cut":
+ this.cut();
+ break;
+ case "cmd_copy":
+ case "placesCmd_copy":
+ this.copy();
+ break;
+ case "cmd_paste":
+ case "placesCmd_paste":
+ this.paste().then(null, Components.utils.reportError);
+ break;
+ case "cmd_delete":
+ case "placesCmd_delete":
+ this.remove("Remove Selection").then(null, Components.utils.reportError);
+ break;
+ case "placesCmd_deleteDataHost":
+ var host;
+ if (PlacesUtils.nodeIsHost(this._view.selectedNode)) {
+ var queries = this._view.selectedNode.getQueries();
+ host = queries[0].domain;
+ }
+ else
+ host = NetUtil.newURI(this._view.selectedNode.uri).host;
+ ForgetAboutSite.removeDataFromDomain(host);
+ break;
+ case "cmd_selectAll":
+ this.selectAll();
+ break;
+ case "placesCmd_open":
+ PlacesUIUtils.openNodeIn(this._view.selectedNode, "current", this._view);
+ break;
+ case "placesCmd_open:window":
+ PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view);
+ break;
+ case "placesCmd_open:privatewindow":
+ PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view, true);
+ break;
+ case "placesCmd_open:tab":
+ PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view);
+ break;
+ case "placesCmd_new:folder":
+ this.newItem("folder");
+ break;
+ case "placesCmd_new:bookmark":
+ this.newItem("bookmark");
+ break;
+ case "placesCmd_new:separator":
+ this.newSeparator().catch(Components.utils.reportError);
+ break;
+ case "placesCmd_show:info":
+ this.showBookmarkPropertiesForSelection();
+ break;
+ case "placesCmd_moveBookmarks":
+ this.moveSelectedBookmarks();
+ break;
+ case "placesCmd_reload":
+ this.reloadSelectedLivemark();
+ break;
+ case "placesCmd_sortBy:name":
+ this.sortFolderByName().then(null, Components.utils.reportError);
+ break;
+ case "placesCmd_createBookmark":
+ let node = this._view.selectedNode;
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "bookmark"
+ , hiddenRows: [ "description"
+ , "keyword"
+ , "location"
+ , "loadInSidebar" ]
+ , uri: NetUtil.newURI(node.uri)
+ , title: node.title
+ }, window.top);
+ break;
+ }
+ },
+
+ onEvent: function PC_onEvent(eventName) { },
+
+
+ /**
+ * Determine whether or not the selection can be removed, either by the
+ * delete or cut operations based on whether or not any of its contents
+ * are non-removable. We don't need to worry about recursion here since it
+ * is a policy decision that a removable item not be placed inside a non-
+ * removable item.
+ *
+ * @return true if all nodes in the selection can be removed,
+ * false otherwise.
+ */
+ _hasRemovableSelection() {
+ var ranges = this._view.removableSelectionRanges;
+ if (!ranges.length)
+ return false;
+
+ var root = this._view.result.root;
+
+ for (var j = 0; j < ranges.length; j++) {
+ var nodes = ranges[j];
+ for (var i = 0; i < nodes.length; ++i) {
+ // Disallow removing the view's root node
+ if (nodes[i] == root)
+ return false;
+
+ if (!PlacesUIUtils.canUserRemove(nodes[i]))
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * Determines whether or not nodes can be inserted relative to the selection.
+ */
+ _canInsert: function PC__canInsert(isPaste) {
+ var ip = this._view.insertionPoint;
+ return ip != null && (isPaste || ip.isTag != true);
+ },
+
+ /**
+ * Looks at the data on the clipboard to see if it is paste-able.
+ * Paste-able data is:
+ * - in a format that the view can receive
+ * @return true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor,
+ * - clipboard data is of type TEXT_UNICODE and
+ * is a valid URI.
+ */
+ _isClipboardDataPasteable: function PC__isClipboardDataPasteable() {
+ // if the clipboard contains TYPE_X_MOZ_PLACE_* data, it is definitely
+ // pasteable, with no need to unwrap all the nodes.
+
+ var flavors = PlacesUIUtils.PLACES_FLAVORS;
+ var clipboard = this.clipboard;
+ var hasPlacesData =
+ clipboard.hasDataMatchingFlavors(flavors, flavors.length,
+ Ci.nsIClipboard.kGlobalClipboard);
+ if (hasPlacesData)
+ return this._view.insertionPoint != null;
+
+ // if the clipboard doesn't have TYPE_X_MOZ_PLACE_* data, we also allow
+ // pasting of valid "text/unicode" and "text/x-moz-url" data
+ var xferable = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ xferable.init(null);
+
+ xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL);
+ xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE);
+ clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
+
+ try {
+ // getAnyTransferData will throw if no data is available.
+ var data = { }, type = { };
+ xferable.getAnyTransferData(type, data, { });
+ data = data.value.QueryInterface(Ci.nsISupportsString).data;
+ if (type.value != PlacesUtils.TYPE_X_MOZ_URL &&
+ type.value != PlacesUtils.TYPE_UNICODE)
+ return false;
+
+ // unwrapNodes() will throw if the data blob is malformed.
+ PlacesUtils.unwrapNodes(data, type.value);
+ return this._view.insertionPoint != null;
+ }
+ catch (e) {
+ // getAnyTransferData or unwrapNodes failed
+ return false;
+ }
+ },
+
+ /**
+ * Gathers information about the selected nodes according to the following
+ * rules:
+ * "link" node is a URI
+ * "bookmark" node is a bookmark
+ * "livemarkChild" node is a child of a livemark
+ * "tagChild" node is a child of a tag
+ * "folder" node is a folder
+ * "query" node is a query
+ * "separator" node is a separator line
+ * "host" node is a host
+ *
+ * @return an array of objects corresponding the selected nodes. Each
+ * object has each of the properties above set if its corresponding
+ * node matches the rule. In addition, the annotations names for each
+ * node are set on its corresponding object as properties.
+ * Notes:
+ * 1) This can be slow, so don't call it anywhere performance critical!
+ */
+ _buildSelectionMetadata: function PC__buildSelectionMetadata() {
+ var metadata = [];
+ var nodes = this._view.selectedNodes;
+
+ for (var i = 0; i < nodes.length; i++) {
+ var nodeData = {};
+ var node = nodes[i];
+ var nodeType = node.type;
+ var uri = null;
+
+ // We don't use the nodeIs* methods here to avoid going through the type
+ // property way too often
+ switch (nodeType) {
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY:
+ nodeData["query"] = true;
+ if (node.parent) {
+ switch (PlacesUtils.asQuery(node.parent).queryOptions.resultType) {
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
+ nodeData["host"] = true;
+ break;
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
+ nodeData["day"] = true;
+ break;
+ }
+ }
+ break;
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT:
+ nodeData["folder"] = true;
+ break;
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
+ nodeData["separator"] = true;
+ break;
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
+ nodeData["link"] = true;
+ uri = NetUtil.newURI(node.uri);
+ if (PlacesUtils.nodeIsBookmark(node)) {
+ nodeData["bookmark"] = true;
+ var parentNode = node.parent;
+ if (parentNode) {
+ if (PlacesUtils.nodeIsTagQuery(parentNode))
+ nodeData["tagChild"] = true;
+ else if (this.hasCachedLivemarkInfo(parentNode))
+ nodeData["livemarkChild"] = true;
+ }
+ }
+ break;
+ }
+
+ // annotations
+ if (uri) {
+ let names = PlacesUtils.annotations.getPageAnnotationNames(uri);
+ for (let j = 0; j < names.length; ++j)
+ nodeData[names[j]] = true;
+ }
+
+ // For items also include the item-specific annotations
+ if (node.itemId != -1) {
+ let names = PlacesUtils.annotations
+ .getItemAnnotationNames(node.itemId);
+ for (let j = 0; j < names.length; ++j)
+ nodeData[names[j]] = true;
+ }
+ metadata.push(nodeData);
+ }
+
+ return metadata;
+ },
+
+ /**
+ * Determines if a context-menu item should be shown
+ * @param aMenuItem
+ * the context menu item
+ * @param aMetaData
+ * meta data about the selection
+ * @return true if the conditions (see buildContextMenu) are satisfied
+ * and the item can be displayed, false otherwise.
+ */
+ _shouldShowMenuItem: function PC__shouldShowMenuItem(aMenuItem, aMetaData) {
+ var selectiontype = aMenuItem.getAttribute("selectiontype");
+ if (!selectiontype) {
+ selectiontype = "single|multiple";
+ }
+ var selectionTypes = selectiontype.split("|");
+ if (selectionTypes.includes("any")) {
+ return true;
+ }
+ var count = aMetaData.length;
+ if (count > 1 && !selectionTypes.includes("multiple"))
+ return false;
+ if (count == 1 && !selectionTypes.includes("single"))
+ return false;
+ // NB: if there is no selection, we show the item if and only if
+ // the selectiontype includes 'none' - the metadata list will be
+ // empty so none of the other criteria will apply anyway.
+ if (count == 0)
+ return selectionTypes.includes("none");
+
+ var forceHideAttr = aMenuItem.getAttribute("forcehideselection");
+ if (forceHideAttr) {
+ var forceHideRules = forceHideAttr.split("|");
+ for (let i = 0; i < aMetaData.length; ++i) {
+ for (let j = 0; j < forceHideRules.length; ++j) {
+ if (forceHideRules[j] in aMetaData[i])
+ return false;
+ }
+ }
+ }
+
+ var selectionAttr = aMenuItem.getAttribute("selection");
+ if (!selectionAttr) {
+ return !aMenuItem.hidden;
+ }
+
+ if (selectionAttr == "any")
+ return true;
+
+ var showRules = selectionAttr.split("|");
+ var anyMatched = false;
+ function metaDataNodeMatches(metaDataNode, rules) {
+ for (var i = 0; i < rules.length; i++) {
+ if (rules[i] in metaDataNode)
+ return true;
+ }
+ return false;
+ }
+
+ for (var i = 0; i < aMetaData.length; ++i) {
+ if (metaDataNodeMatches(aMetaData[i], showRules))
+ anyMatched = true;
+ else
+ return false;
+ }
+ return anyMatched;
+ },
+
+ /**
+ * Detects information (meta-data rules) about the current selection in the
+ * view (see _buildSelectionMetadata) and sets the visibility state for each
+ * of the menu-items in the given popup with the following rules applied:
+ * 0) The "ignoreitem" attribute may be set to "true" for this code not to
+ * handle that menuitem.
+ * 1) The "selectiontype" attribute may be set on a menu-item to "single"
+ * if the menu-item should be visible only if there is a single node
+ * selected, or to "multiple" if the menu-item should be visible only if
+ * multiple nodes are selected, or to "none" if the menuitems should be
+ * visible for if there are no selected nodes, or to a |-separated
+ * combination of these.
+ * If the attribute is not set or set to an invalid value, the menu-item
+ * may be visible irrespective of the selection.
+ * 2) The "selection" attribute may be set on a menu-item to the various
+ * meta-data rules for which it may be visible. The rules should be
+ * separated with the | character.
+ * 3) A menu-item may be visible only if at least one of the rules set in
+ * its selection attribute apply to each of the selected nodes in the
+ * view.
+ * 4) The "forcehideselection" attribute may be set on a menu-item to rules
+ * for which it should be hidden. This attribute takes priority over the
+ * selection attribute. A menu-item would be hidden if at least one of the
+ * given rules apply to one of the selected nodes. The rules should be
+ * separated with the | character.
+ * 5) The "hideifnoinsertionpoint" attribute may be set on a menu-item to
+ * true if it should be hidden when there's no insertion point
+ * 6) The visibility state of a menu-item is unchanged if none of these
+ * attribute are set.
+ * 7) These attributes should not be set on separators for which the
+ * visibility state is "auto-detected."
+ * 8) The "hideifprivatebrowsing" attribute may be set on a menu-item to
+ * true if it should be hidden inside the private browsing mode
+ * @param aPopup
+ * The menupopup to build children into.
+ * @return true if at least one item is visible, false otherwise.
+ */
+ buildContextMenu: function PC_buildContextMenu(aPopup) {
+ var metadata = this._buildSelectionMetadata();
+ var ip = this._view.insertionPoint;
+ var noIp = !ip || ip.isTag;
+
+ var separator = null;
+ var visibleItemsBeforeSep = false;
+ var usableItemCount = 0;
+ for (var i = 0; i < aPopup.childNodes.length; ++i) {
+ var item = aPopup.childNodes[i];
+ if (item.getAttribute("ignoreitem") == "true") {
+ continue;
+ }
+ if (item.localName != "menuseparator") {
+ // We allow pasting into tag containers, so special case that.
+ var hideIfNoIP = item.getAttribute("hideifnoinsertionpoint") == "true" &&
+ noIp && !(ip && ip.isTag && item.id == "placesContext_paste");
+ var hideIfPrivate = item.getAttribute("hideifprivatebrowsing") == "true" &&
+ PrivateBrowsingUtils.isWindowPrivate(window);
+ var shouldHideItem = hideIfNoIP || hideIfPrivate ||
+ !this._shouldShowMenuItem(item, metadata);
+ item.hidden = item.disabled = shouldHideItem;
+
+ if (!item.hidden) {
+ visibleItemsBeforeSep = true;
+ usableItemCount++;
+
+ // Show the separator above the menu-item if any
+ if (separator) {
+ separator.hidden = false;
+ separator = null;
+ }
+ }
+ }
+ else { // menuseparator
+ // Initially hide it. It will be unhidden if there will be at least one
+ // visible menu-item above and below it.
+ item.hidden = true;
+
+ // We won't show the separator at all if no items are visible above it
+ if (visibleItemsBeforeSep)
+ separator = item;
+
+ // New separator, count again:
+ visibleItemsBeforeSep = false;
+ }
+ }
+
+ // Set Open Folder/Links In Tabs items enabled state if they're visible
+ if (usableItemCount > 0) {
+ var openContainerInTabsItem = document.getElementById("placesContext_openContainer:tabs");
+ if (!openContainerInTabsItem.hidden) {
+ var containerToUse = this._view.selectedNode || this._view.result.root;
+ if (PlacesUtils.nodeIsContainer(containerToUse)) {
+ if (!PlacesUtils.hasChildURIs(containerToUse)) {
+ openContainerInTabsItem.disabled = true;
+ // Ensure that we don't display the menu if nothing is enabled:
+ usableItemCount--;
+ }
+ }
+ }
+ }
+
+ return usableItemCount > 0;
+ },
+
+ /**
+ * Select all links in the current view.
+ */
+ selectAll: function PC_selectAll() {
+ this._view.selectAll();
+ },
+
+ /**
+ * Opens the bookmark properties for the selected URI Node.
+ */
+ showBookmarkPropertiesForSelection() {
+ let node = this._view.selectedNode;
+ if (!node)
+ return;
+
+ PlacesUIUtils.showBookmarkDialog({ action: "edit"
+ , node
+ , hiddenRows: [ "folderPicker" ]
+ }, window.top);
+ },
+
+ /**
+ * This method can be run on a URI parameter to ensure that it didn't
+ * receive a string instead of an nsIURI object.
+ */
+ _assertURINotString: function PC__assertURINotString(value) {
+ NS_ASSERT((typeof(value) == "object") && !(value instanceof String),
+ "This method should be passed a URI as a nsIURI object, not as a string.");
+ },
+
+ /**
+ * Reloads the selected livemark if any.
+ */
+ reloadSelectedLivemark: function PC_reloadSelectedLivemark() {
+ var selectedNode = this._view.selectedNode;
+ if (selectedNode) {
+ let itemId = selectedNode.itemId;
+ PlacesUtils.livemarks.getLivemark({ id: itemId })
+ .then(aLivemark => {
+ aLivemark.reload(true);
+ }, Components.utils.reportError);
+ }
+ },
+
+ /**
+ * Opens the links in the selected folder, or the selected links in new tabs.
+ */
+ openSelectionInTabs: function PC_openLinksInTabs(aEvent) {
+ var node = this._view.selectedNode;
+ var nodes = this._view.selectedNodes;
+ // In the case of no selection, open the root node:
+ if (!node && !nodes.length) {
+ node = this._view.result.root;
+ }
+ if (node && PlacesUtils.nodeIsContainer(node))
+ PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this._view);
+ else
+ PlacesUIUtils.openURINodesInTabs(nodes, aEvent, this._view);
+ },
+
+ /**
+ * Shows the Add Bookmark UI for the current insertion point.
+ *
+ * @param aType
+ * the type of the new item (bookmark/livemark/folder)
+ */
+ newItem: function PC_newItem(aType) {
+ let ip = this._view.insertionPoint;
+ if (!ip)
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ let performed =
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: aType
+ , defaultInsertionPoint: ip
+ , hiddenRows: [ "folderPicker" ]
+ }, window.top);
+ if (performed) {
+ // Select the new item.
+ let insertedNodeId = PlacesUtils.bookmarks
+ .getIdForItemAt(ip.itemId, ip.index);
+ this._view.selectItems([insertedNodeId], false);
+ }
+ },
+
+ /**
+ * Create a new Bookmark separator somewhere.
+ */
+ newSeparator: Task.async(function* () {
+ var ip = this._view.insertionPoint;
+ if (!ip)
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ // Select the new item.
+ let insertedNodeId = PlacesUtils.bookmarks
+ .getIdForItemAt(ip.itemId, ip.index);
+ this._view.selectItems([insertedNodeId], false);
+ return;
+ }
+
+ let txn = PlacesTransactions.NewSeparator({ parentGuid: yield ip.promiseGuid()
+ , index: ip.index });
+ let guid = yield txn.transact();
+ let itemId = yield PlacesUtils.promiseItemId(guid);
+ // Select the new item.
+ this._view.selectItems([itemId], false);
+ }),
+
+ /**
+ * Opens a dialog for moving the selected nodes.
+ */
+ moveSelectedBookmarks: function PC_moveBookmarks() {
+ window.openDialog("chrome://browser/content/places/moveBookmarks.xul",
+ "", "chrome, modal",
+ this._view.selectedNodes);
+ },
+
+ /**
+ * Sort the selected folder by name
+ */
+ sortFolderByName: Task.async(function* () {
+ let itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode);
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ var txn = new PlacesSortFolderByNameTransaction(itemId);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ return;
+ }
+ let guid = yield PlacesUtils.promiseItemGuid(itemId);
+ yield PlacesTransactions.SortByName(guid).transact();
+ }),
+
+ /**
+ * Walk the list of folders we're removing in this delete operation, and
+ * see if the selected node specified is already implicitly being removed
+ * because it is a child of that folder.
+ * @param node
+ * Node to check for containment.
+ * @param pastFolders
+ * List of folders the calling function has already traversed
+ * @return true if the node should be skipped, false otherwise.
+ */
+ _shouldSkipNode: function PC_shouldSkipNode(node, pastFolders) {
+ /**
+ * Determines if a node is contained by another node within a resultset.
+ * @param node
+ * The node to check for containment for
+ * @param parent
+ * The parent container to check for containment in
+ * @return true if node is a member of parent's children, false otherwise.
+ */
+ function isContainedBy(node, parent) {
+ var cursor = node.parent;
+ while (cursor) {
+ if (cursor == parent)
+ return true;
+ cursor = cursor.parent;
+ }
+ return false;
+ }
+
+ for (var j = 0; j < pastFolders.length; ++j) {
+ if (isContainedBy(node, pastFolders[j]))
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Creates a set of transactions for the removal of a range of items.
+ * A range is an array of adjacent nodes in a view.
+ * @param [in] range
+ * An array of nodes to remove. Should all be adjacent.
+ * @param [out] transactions
+ * An array of transactions.
+ * @param [optional] removedFolders
+ * An array of folder nodes that have already been removed.
+ */
+ _removeRange: function PC__removeRange(range, transactions, removedFolders) {
+ NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
+ if (!removedFolders)
+ removedFolders = [];
+
+ for (var i = 0; i < range.length; ++i) {
+ var node = range[i];
+ if (this._shouldSkipNode(node, removedFolders))
+ continue;
+
+ if (PlacesUtils.nodeIsTagQuery(node.parent)) {
+ // This is a uri node inside a tag container. It needs a special
+ // untag transaction.
+ var tagItemId = PlacesUtils.getConcreteItemId(node.parent);
+ var uri = NetUtil.newURI(node.uri);
+ if (PlacesUIUtils.useAsyncTransactions) {
+ let tag = node.parent.title;
+ if (!tag)
+ tag = PlacesUtils.bookmarks.getItemTitle(tagItemId);
+ transactions.push(PlacesTransactions.Untag({ uri: uri, tag: tag }));
+ }
+ else {
+ let txn = new PlacesUntagURITransaction(uri, [tagItemId]);
+ transactions.push(txn);
+ }
+ }
+ else if (PlacesUtils.nodeIsTagQuery(node) && node.parent &&
+ PlacesUtils.nodeIsQuery(node.parent) &&
+ PlacesUtils.asQuery(node.parent).queryOptions.resultType ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) {
+ // This is a tag container.
+ // Untag all URIs tagged with this tag only if the tag container is
+ // child of the "Tags" query in the library, in all other places we
+ // must only remove the query node.
+ let tag = node.title;
+ let URIs = PlacesUtils.tagging.getURIsForTag(tag);
+ if (PlacesUIUtils.useAsyncTransactions) {
+ transactions.push(PlacesTransactions.Untag({ tag: tag, uris: URIs }));
+ }
+ else {
+ for (var j = 0; j < URIs.length; j++) {
+ let txn = new PlacesUntagURITransaction(URIs[j], [tag]);
+ transactions.push(txn);
+ }
+ }
+ }
+ else if (PlacesUtils.nodeIsURI(node) &&
+ PlacesUtils.nodeIsQuery(node.parent) &&
+ PlacesUtils.asQuery(node.parent).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ // This is a uri node inside an history query.
+ PlacesUtils.bhistory.removePage(NetUtil.newURI(node.uri));
+ // History deletes are not undoable, so we don't have a transaction.
+ }
+ else if (node.itemId == -1 &&
+ PlacesUtils.nodeIsQuery(node) &&
+ PlacesUtils.asQuery(node).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ // This is a dynamically generated history query, like queries
+ // grouped by site, time or both. Dynamically generated queries don't
+ // have an itemId even if they are descendants of a bookmark.
+ this._removeHistoryContainer(node);
+ // History deletes are not undoable, so we don't have a transaction.
+ }
+ else {
+ // This is a common bookmark item.
+ if (PlacesUtils.nodeIsFolder(node)) {
+ // If this is a folder we add it to our array of folders, used
+ // to skip nodes that are children of an already removed folder.
+ removedFolders.push(node);
+ }
+ if (PlacesUIUtils.useAsyncTransactions) {
+ transactions.push(
+ PlacesTransactions.Remove({ guid: node.bookmarkGuid }));
+ }
+ else {
+ let txn = new PlacesRemoveItemTransaction(node.itemId);
+ transactions.push(txn);
+ }
+ }
+ }
+ },
+
+ /**
+ * Removes the set of selected ranges from bookmarks.
+ * @param txnName
+ * See |remove|.
+ */
+ _removeRowsFromBookmarks: Task.async(function* (txnName) {
+ var ranges = this._view.removableSelectionRanges;
+ var transactions = [];
+ var removedFolders = [];
+
+ for (var i = 0; i < ranges.length; i++)
+ this._removeRange(ranges[i], transactions, removedFolders);
+
+ if (transactions.length > 0) {
+ if (PlacesUIUtils.useAsyncTransactions) {
+ yield PlacesTransactions.batch(transactions);
+ }
+ else {
+ var txn = new PlacesAggregatedTransaction(txnName, transactions);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ }
+ }
+ }),
+
+ /**
+ * Removes the set of selected ranges from history.
+ *
+ * @note history deletes are not undoable.
+ */
+ _removeRowsFromHistory: function PC__removeRowsFromHistory() {
+ let nodes = this._view.selectedNodes;
+ let URIs = [];
+ for (let i = 0; i < nodes.length; ++i) {
+ let node = nodes[i];
+ if (PlacesUtils.nodeIsURI(node)) {
+ let uri = NetUtil.newURI(node.uri);
+ // Avoid duplicates.
+ if (URIs.indexOf(uri) < 0) {
+ URIs.push(uri);
+ }
+ }
+ else if (PlacesUtils.nodeIsQuery(node) &&
+ PlacesUtils.asQuery(node).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ this._removeHistoryContainer(node);
+ }
+ }
+
+ // Do removal in chunks to give some breath to main-thread.
+ function* pagesChunkGenerator(aURIs) {
+ while (aURIs.length) {
+ let URIslice = aURIs.splice(0, REMOVE_PAGES_CHUNKLEN);
+ PlacesUtils.bhistory.removePages(URIslice, URIslice.length);
+ Services.tm.mainThread.dispatch(() => gen.next(),
+ Ci.nsIThread.DISPATCH_NORMAL);
+ yield undefined;
+ }
+ }
+ let gen = pagesChunkGenerator(URIs);
+ gen.next();
+ },
+
+ /**
+ * Removes history visits for an history container node.
+ * @param [in] aContainerNode
+ * The container node to remove.
+ *
+ * @note history deletes are not undoable.
+ */
+ _removeHistoryContainer: function PC__removeHistoryContainer(aContainerNode) {
+ if (PlacesUtils.nodeIsHost(aContainerNode)) {
+ // Site container.
+ PlacesUtils.bhistory.removePagesFromHost(aContainerNode.title, true);
+ }
+ else if (PlacesUtils.nodeIsDay(aContainerNode)) {
+ // Day container.
+ let query = aContainerNode.getQueries()[0];
+ let beginTime = query.beginTime;
+ let endTime = query.endTime;
+ NS_ASSERT(query && beginTime && endTime,
+ "A valid date container query should exist!");
+ // We want to exclude beginTime from the removal because
+ // removePagesByTimeframe includes both extremes, while date containers
+ // exclude the lower extreme. So, if we would not exclude it, we would
+ // end up removing more history than requested.
+ PlacesUtils.bhistory.removePagesByTimeframe(beginTime + 1, endTime);
+ }
+ },
+
+ /**
+ * Removes the selection
+ * @param aTxnName
+ * A name for the transaction if this is being performed
+ * as part of another operation.
+ */
+ remove: Task.async(function* (aTxnName) {
+ if (!this._hasRemovableSelection())
+ return;
+
+ NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name");
+
+ var root = this._view.result.root;
+
+ if (PlacesUtils.nodeIsFolder(root)) {
+ if (PlacesUIUtils.useAsyncTransactions)
+ yield this._removeRowsFromBookmarks(aTxnName);
+ else
+ this._removeRowsFromBookmarks(aTxnName);
+ }
+ else if (PlacesUtils.nodeIsQuery(root)) {
+ var queryType = PlacesUtils.asQuery(root).queryOptions.queryType;
+ if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS) {
+ if (PlacesUIUtils.useAsyncTransactions)
+ yield this._removeRowsFromBookmarks(aTxnName);
+ else
+ this._removeRowsFromBookmarks(aTxnName);
+ }
+ else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ this._removeRowsFromHistory();
+ }
+ else {
+ NS_ASSERT(false, "implement support for QUERY_TYPE_UNIFIED");
+ }
+ }
+ else
+ NS_ASSERT(false, "unexpected root");
+ }),
+
+ /**
+ * Fills a DataTransfer object with the content of the selection that can be
+ * dropped elsewhere.
+ * @param aEvent
+ * The dragstart event.
+ */
+ setDataTransfer: function PC_setDataTransfer(aEvent) {
+ let dt = aEvent.dataTransfer;
+
+ let result = this._view.result;
+ let didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+
+ function addData(type, index, feedURI) {
+ let wrapNode = PlacesUtils.wrapNode(node, type, feedURI);
+ dt.mozSetDataAt(type, wrapNode, index);
+ }
+
+ function addURIData(index, feedURI) {
+ addData(PlacesUtils.TYPE_X_MOZ_URL, index, feedURI);
+ addData(PlacesUtils.TYPE_UNICODE, index, feedURI);
+ addData(PlacesUtils.TYPE_HTML, index, feedURI);
+ }
+
+ try {
+ let nodes = this._view.draggableSelection;
+ for (let i = 0; i < nodes.length; ++i) {
+ var node = nodes[i];
+
+ // This order is _important_! It controls how this and other
+ // applications select data to be inserted based on type.
+ addData(PlacesUtils.TYPE_X_MOZ_PLACE, i);
+
+ // Drop the feed uri for livemark containers
+ let livemarkInfo = this.getCachedLivemarkInfo(node);
+ if (livemarkInfo) {
+ addURIData(i, livemarkInfo.feedURI.spec);
+ }
+ else if (node.uri) {
+ addURIData(i);
+ }
+ }
+ }
+ finally {
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+ },
+
+ get clipboardAction () {
+ let action = {};
+ let actionOwner;
+ try {
+ let xferable = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ xferable.init(null);
+ xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION)
+ this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
+ xferable.getTransferData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, action, {});
+ [action, actionOwner] =
+ action.value.QueryInterface(Ci.nsISupportsString).data.split(",");
+ } catch (ex) {
+ // Paste from external sources don't have any associated action, just
+ // fallback to a copy action.
+ return "copy";
+ }
+ // For cuts also check who inited the action, since cuts across different
+ // instances should instead be handled as copies (The sources are not
+ // available for this instance).
+ if (action == "cut" && actionOwner != this.profileName)
+ action = "copy";
+
+ return action;
+ },
+
+ _releaseClipboardOwnership: function PC__releaseClipboardOwnership() {
+ if (this.cutNodes.length > 0) {
+ // This clears the logical clipboard, doesn't remove data.
+ this.clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
+ }
+ },
+
+ _clearClipboard: function PC__clearClipboard() {
+ let xferable = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ xferable.init(null);
+ // Empty transferables may cause crashes, so just add an unknown type.
+ const TYPE = "text/x-moz-place-empty";
+ xferable.addDataFlavor(TYPE);
+ xferable.setTransferData(TYPE, PlacesUtils.toISupportsString(""), 0);
+ this.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
+ },
+
+ _populateClipboard: function PC__populateClipboard(aNodes, aAction) {
+ // This order is _important_! It controls how this and other applications
+ // select data to be inserted based on type.
+ let contents = [
+ { type: PlacesUtils.TYPE_X_MOZ_PLACE, entries: [] },
+ { type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] },
+ { type: PlacesUtils.TYPE_HTML, entries: [] },
+ { type: PlacesUtils.TYPE_UNICODE, entries: [] },
+ ];
+
+ // Avoid handling descendants of a copied node, the transactions take care
+ // of them automatically.
+ let copiedFolders = [];
+ aNodes.forEach(function (node) {
+ if (this._shouldSkipNode(node, copiedFolders))
+ return;
+ if (PlacesUtils.nodeIsFolder(node))
+ copiedFolders.push(node);
+
+ let livemarkInfo = this.getCachedLivemarkInfo(node);
+ let feedURI = livemarkInfo && livemarkInfo.feedURI.spec;
+
+ contents.forEach(function (content) {
+ content.entries.push(
+ PlacesUtils.wrapNode(node, content.type, feedURI)
+ );
+ });
+ }, this);
+
+ function addData(type, data) {
+ xferable.addDataFlavor(type);
+ xferable.setTransferData(type, PlacesUtils.toISupportsString(data),
+ data.length * 2);
+ }
+
+ let xferable = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ xferable.init(null);
+ let hasData = false;
+ // This order matters here! It controls how this and other applications
+ // select data to be inserted based on type.
+ contents.forEach(function (content) {
+ if (content.entries.length > 0) {
+ hasData = true;
+ let glue =
+ content.type == PlacesUtils.TYPE_X_MOZ_PLACE ? "," : PlacesUtils.endl;
+ addData(content.type, content.entries.join(glue));
+ }
+ });
+
+ // Track the exected action in the xferable. This must be the last flavor
+ // since it's the least preferred one.
+ // Enqueue a unique instance identifier to distinguish operations across
+ // concurrent instances of the application.
+ addData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, aAction + "," + this.profileName);
+
+ if (hasData) {
+ this.clipboard.setData(xferable,
+ this.cutNodes.length > 0 ? this : null,
+ Ci.nsIClipboard.kGlobalClipboard);
+ }
+ },
+
+ _cutNodes: [],
+ get cutNodes() {
+ return this._cutNodes;
+ },
+ set cutNodes(aNodes) {
+ let self = this;
+ function updateCutNodes(aValue) {
+ self._cutNodes.forEach(function (aNode) {
+ self._view.toggleCutNode(aNode, aValue);
+ });
+ }
+
+ updateCutNodes(false);
+ this._cutNodes = aNodes;
+ updateCutNodes(true);
+ return aNodes;
+ },
+
+ /**
+ * Copy Bookmarks and Folders to the clipboard
+ */
+ copy: function PC_copy() {
+ let result = this._view.result;
+ let didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+ try {
+ this._populateClipboard(this._view.selectedNodes, "copy");
+ }
+ finally {
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+ },
+
+ /**
+ * Cut Bookmarks and Folders to the clipboard
+ */
+ cut: function PC_cut() {
+ let result = this._view.result;
+ let didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+ try {
+ this._populateClipboard(this._view.selectedNodes, "cut");
+ this.cutNodes = this._view.selectedNodes;
+ }
+ finally {
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+ },
+
+ /**
+ * Paste Bookmarks and Folders from the clipboard
+ */
+ paste: Task.async(function* () {
+ // No reason to proceed if there isn't a valid insertion point.
+ let ip = this._view.insertionPoint;
+ if (!ip)
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ let action = this.clipboardAction;
+
+ let xferable = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ xferable.init(null);
+ // This order matters here! It controls the preferred flavors for this
+ // paste operation.
+ [ PlacesUtils.TYPE_X_MOZ_PLACE,
+ PlacesUtils.TYPE_X_MOZ_URL,
+ PlacesUtils.TYPE_UNICODE,
+ ].forEach(type => xferable.addDataFlavor(type));
+
+ this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
+
+ // Now get the clipboard contents, in the best available flavor.
+ let data = {}, type = {}, items = [];
+ try {
+ xferable.getAnyTransferData(type, data, {});
+ data = data.value.QueryInterface(Ci.nsISupportsString).data;
+ type = type.value;
+ items = PlacesUtils.unwrapNodes(data, type);
+ } catch (ex) {
+ // No supported data exists or nodes unwrap failed, just bail out.
+ return;
+ }
+
+ let itemsToSelect = [];
+ if (PlacesUIUtils.useAsyncTransactions) {
+ if (ip.isTag) {
+ let uris = items.filter(item => "uri" in item).map(item => NetUtil.newURI(item.uri));
+ yield PlacesTransactions.Tag({ uris: uris, tag: ip.tagName }).transact();
+ }
+ else {
+ yield PlacesTransactions.batch(function* () {
+ let insertionIndex = ip.index;
+ let parent = yield ip.promiseGuid();
+
+ for (let item of items) {
+ let doCopy = action == "copy";
+
+ // If this is not a copy, check for safety that we can move the
+ // source, otherwise report an error and fallback to a copy.
+ if (!doCopy &&
+ !PlacesControllerDragHelper.canMoveUnwrappedNode(item)) {
+ Components.utils.reportError("Tried to move an unmovable " +
+ "Places node, reverting to a copy operation.");
+ doCopy = true;
+ }
+ let guid = yield PlacesUIUtils.getTransactionForData(
+ item, type, parent, insertionIndex, doCopy).transact();
+ itemsToSelect.push(yield PlacesUtils.promiseItemId(guid));
+
+ // Adjust index to make sure items are pasted in the correct
+ // position. If index is DEFAULT_INDEX, items are just appended.
+ if (insertionIndex != PlacesUtils.bookmarks.DEFAULT_INDEX)
+ insertionIndex++;
+ }
+ });
+ }
+ }
+ else {
+ let transactions = [];
+ let insertionIndex = ip.index;
+ for (let i = 0; i < items.length; ++i) {
+ if (ip.isTag) {
+ // Pasting into a tag container means tagging the item, regardless of
+ // the requested action.
+ let tagTxn = new PlacesTagURITransaction(NetUtil.newURI(items[i].uri),
+ [ip.itemId]);
+ transactions.push(tagTxn);
+ continue;
+ }
+
+ // Adjust index to make sure items are pasted in the correct position.
+ // If index is DEFAULT_INDEX, items are just appended.
+ if (ip.index != PlacesUtils.bookmarks.DEFAULT_INDEX)
+ insertionIndex = ip.index + i;
+
+ // If this is not a copy, check for safety that we can move the source,
+ // otherwise report an error and fallback to a copy.
+ if (action != "copy" && !PlacesControllerDragHelper.canMoveUnwrappedNode(items[i])) {
+ Components.utils.reportError("Tried to move an unmovable Places " +
+ "node, reverting to a copy operation.");
+ action = "copy";
+ }
+ transactions.push(
+ PlacesUIUtils.makeTransaction(items[i], type, ip.itemId,
+ insertionIndex, action == "copy")
+ );
+ }
+
+ let aggregatedTxn = new PlacesAggregatedTransaction("Paste", transactions);
+ PlacesUtils.transactionManager.doTransaction(aggregatedTxn);
+
+ for (let i = 0; i < transactions.length; ++i) {
+ itemsToSelect.push(
+ PlacesUtils.bookmarks.getIdForItemAt(ip.itemId, ip.index + i)
+ );
+ }
+ }
+
+ // Cut/past operations are not repeatable, so clear the clipboard.
+ if (action == "cut") {
+ this._clearClipboard();
+ }
+
+ if (itemsToSelect.length > 0)
+ this._view.selectItems(itemsToSelect, false);
+ }),
+
+ /**
+ * Cache the livemark info for a node. This allows the controller and the
+ * views to treat the given node as a livemark.
+ * @param aNode
+ * a places result node.
+ * @param aLivemarkInfo
+ * a mozILivemarkInfo object.
+ */
+ cacheLivemarkInfo: function PC_cacheLivemarkInfo(aNode, aLivemarkInfo) {
+ this._cachedLivemarkInfoObjects.set(aNode, aLivemarkInfo);
+ },
+
+ /**
+ * Returns whether or not there's cached mozILivemarkInfo object for a node.
+ * @param aNode
+ * a places result node.
+ * @return true if there's a cached mozILivemarkInfo object for
+ * aNode, false otherwise.
+ */
+ hasCachedLivemarkInfo: function PC_hasCachedLivemarkInfo(aNode) {
+ return this._cachedLivemarkInfoObjects.has(aNode);
+ },
+
+ /**
+ * Returns the cached livemark info for a node, if set by cacheLivemarkInfo,
+ * null otherwise.
+ * @param aNode
+ * a places result node.
+ * @return the mozILivemarkInfo object for aNode, if set, null otherwise.
+ */
+ getCachedLivemarkInfo: function PC_getCachedLivemarkInfo(aNode) {
+ return this._cachedLivemarkInfoObjects.get(aNode, null);
+ }
+};
+
+/**
+ * Handles drag and drop operations for views. Note that this is view agnostic!
+ * You should not use PlacesController._view within these methods, since
+ * the view that the item(s) have been dropped on was not necessarily active.
+ * Drop functions are passed the view that is being dropped on.
+ */
+var PlacesControllerDragHelper = {
+ /**
+ * DOM Element currently being dragged over
+ */
+ currentDropTarget: null,
+
+ /**
+ * Determines if the mouse is currently being dragged over a child node of
+ * this menu. This is necessary so that the menu doesn't close while the
+ * mouse is dragging over one of its submenus
+ * @param node
+ * The container node
+ * @return true if the user is dragging over a node within the hierarchy of
+ * the container, false otherwise.
+ */
+ draggingOverChildNode: function PCDH_draggingOverChildNode(node) {
+ let currentNode = this.currentDropTarget;
+ while (currentNode) {
+ if (currentNode == node)
+ return true;
+ currentNode = currentNode.parentNode;
+ }
+ return false;
+ },
+
+ /**
+ * @return The current active drag session. Returns null if there is none.
+ */
+ getSession: function PCDH__getSession() {
+ return this.dragService.getCurrentSession();
+ },
+
+ /**
+ * Extract the first accepted flavor from a list of flavors.
+ * @param aFlavors
+ * The flavors list of type DOMStringList.
+ */
+ getFirstValidFlavor: function PCDH_getFirstValidFlavor(aFlavors) {
+ for (let i = 0; i < aFlavors.length; i++) {
+ if (PlacesUIUtils.SUPPORTED_FLAVORS.includes(aFlavors[i]))
+ return aFlavors[i];
+ }
+
+ // If no supported flavor is found, check if data includes text/plain
+ // contents. If so, request them as text/unicode, a conversion will happen
+ // automatically.
+ if (aFlavors.contains("text/plain")) {
+ return PlacesUtils.TYPE_UNICODE;
+ }
+
+ return null;
+ },
+
+ /**
+ * Determines whether or not the data currently being dragged can be dropped
+ * on a places view.
+ * @param ip
+ * The insertion point where the items should be dropped.
+ */
+ canDrop: function PCDH_canDrop(ip, dt) {
+ let dropCount = dt.mozItemCount;
+
+ // Check every dragged item.
+ for (let i = 0; i < dropCount; i++) {
+ let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
+ if (!flavor)
+ return false;
+
+ // Urls can be dropped on any insertionpoint.
+ // XXXmano: remember that this method is called for each dragover event!
+ // Thus we shouldn't use unwrapNodes here at all if possible.
+ // I think it would be OK to accept bogus data here (e.g. text which was
+ // somehow wrapped as TAB_DROP_TYPE, this is not in our control, and
+ // will just case the actual drop to be a no-op), and only rule out valid
+ // expected cases, which are either unsupported flavors, or items which
+ // cannot be dropped in the current insertionpoint. The last case will
+ // likely force us to use unwrapNodes for the private data types of
+ // places.
+ if (flavor == TAB_DROP_TYPE)
+ continue;
+
+ let data = dt.mozGetDataAt(flavor, i);
+ let dragged;
+ try {
+ dragged = PlacesUtils.unwrapNodes(data, flavor)[0];
+ }
+ catch (e) {
+ return false;
+ }
+
+ // Only bookmarks and urls can be dropped into tag containers.
+ if (ip.isTag &&
+ dragged.type != PlacesUtils.TYPE_X_MOZ_URL &&
+ (dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE ||
+ (dragged.uri && dragged.uri.startsWith("place:")) ))
+ return false;
+
+ // The following loop disallows the dropping of a folder on itself or
+ // on any of its descendants.
+ if (dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER ||
+ (dragged.uri && dragged.uri.startsWith("place:")) ) {
+ let parentId = ip.itemId;
+ while (parentId != PlacesUtils.placesRootId) {
+ if (dragged.concreteId == parentId || dragged.id == parentId)
+ return false;
+ parentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId);
+ }
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Determines if an unwrapped node can be moved.
+ *
+ * @param aUnwrappedNode
+ * A node unwrapped by PlacesUtils.unwrapNodes().
+ * @return True if the node can be moved, false otherwise.
+ */
+ canMoveUnwrappedNode: function (aUnwrappedNode) {
+ return aUnwrappedNode.id > 0 &&
+ !PlacesUtils.isRootItem(aUnwrappedNode.id) &&
+ (!aUnwrappedNode.parent || !PlacesUIUtils.isContentsReadOnly(aUnwrappedNode.parent)) &&
+ aUnwrappedNode.parent != PlacesUtils.tagsFolderId &&
+ aUnwrappedNode.grandParentId != PlacesUtils.tagsFolderId;
+ },
+
+ /**
+ * Determines if a node can be moved.
+ *
+ * @param aNode
+ * A nsINavHistoryResultNode node.
+ * @param [optional] aDOMNode
+ * A XUL DOM node.
+ * @return True if the node can be moved, false otherwise.
+ */
+ canMoveNode(aNode, aDOMNode) {
+ // Only bookmark items are movable.
+ if (aNode.itemId == -1)
+ return false;
+
+ let parentNode = aNode.parent;
+ if (!parentNode) {
+ // Normally parentless places nodes can not be moved,
+ // but simulated bookmarked URI nodes are special.
+ return !!aDOMNode &&
+ aDOMNode.hasAttribute("simulated-places-node") &&
+ PlacesUtils.nodeIsBookmark(aNode);
+ }
+
+ // Once tags and bookmarked are divorced, the tag-query check should be
+ // removed.
+ return !(PlacesUtils.nodeIsFolder(parentNode) &&
+ PlacesUIUtils.isContentsReadOnly(parentNode)) &&
+ !PlacesUtils.nodeIsTagQuery(parentNode);
+ },
+
+ /**
+ * Handles the drop of one or more items onto a view.
+ * @param insertionPoint
+ * The insertion point where the items should be dropped
+ */
+ onDrop: Task.async(function* (insertionPoint, dt) {
+ let doCopy = ["copy", "link"].includes(dt.dropEffect);
+
+ let transactions = [];
+ let dropCount = dt.mozItemCount;
+ let movedCount = 0;
+ let parentGuid = PlacesUIUtils.useAsyncTransactions ?
+ (yield insertionPoint.promiseGuid()) : null;
+ let tagName = insertionPoint.tagName;
+ for (let i = 0; i < dropCount; ++i) {
+ let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
+ if (!flavor)
+ return;
+
+ let data = dt.mozGetDataAt(flavor, i);
+ let unwrapped;
+ if (flavor != TAB_DROP_TYPE) {
+ // There's only ever one in the D&D case.
+ unwrapped = PlacesUtils.unwrapNodes(data, flavor)[0];
+ }
+ else if (data instanceof XULElement && data.localName == "tab" &&
+ data.ownerGlobal instanceof ChromeWindow) {
+ let uri = data.linkedBrowser.currentURI;
+ let spec = uri ? uri.spec : "about:blank";
+ unwrapped = { uri: spec,
+ title: data.label,
+ type: PlacesUtils.TYPE_X_MOZ_URL};
+ }
+ else
+ throw new Error("bogus data was passed as a tab");
+
+ let index = insertionPoint.index;
+
+ // Adjust insertion index to prevent reversal of dragged items. When you
+ // drag multiple elts upward: need to increment index or each successive
+ // elt will be inserted at the same index, each above the previous.
+ let dragginUp = insertionPoint.itemId == unwrapped.parent &&
+ index < PlacesUtils.bookmarks.getItemIndex(unwrapped.id);
+ if (index != -1 && dragginUp)
+ index+= movedCount++;
+
+ // If dragging over a tag container we should tag the item.
+ if (insertionPoint.isTag) {
+ let uri = NetUtil.newURI(unwrapped.uri);
+ let tagItemId = insertionPoint.itemId;
+ if (PlacesUIUtils.useAsyncTransactions)
+ transactions.push(PlacesTransactions.Tag({ uri: uri, tag: tagName }));
+ else
+ transactions.push(new PlacesTagURITransaction(uri, [tagItemId]));
+ }
+ else {
+ // If this is not a copy, check for safety that we can move the source,
+ // otherwise report an error and fallback to a copy.
+ if (!doCopy && !PlacesControllerDragHelper.canMoveUnwrappedNode(unwrapped)) {
+ Components.utils.reportError("Tried to move an unmovable Places " +
+ "node, reverting to a copy operation.");
+ doCopy = true;
+ }
+ if (PlacesUIUtils.useAsyncTransactions) {
+ transactions.push(
+ PlacesUIUtils.getTransactionForData(unwrapped,
+ flavor,
+ parentGuid,
+ index,
+ doCopy));
+ }
+ else {
+ transactions.push(PlacesUIUtils.makeTransaction(unwrapped,
+ flavor, insertionPoint.itemId,
+ index, doCopy));
+ }
+ }
+ }
+
+ if (PlacesUIUtils.useAsyncTransactions) {
+ yield PlacesTransactions.batch(transactions);
+ }
+ else {
+ let txn = new PlacesAggregatedTransaction("DropItems", transactions);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ }
+ }),
+
+ /**
+ * Checks if we can insert into a container.
+ * @param aContainer
+ * The container were we are want to drop
+ */
+ disallowInsertion: function(aContainer) {
+ NS_ASSERT(aContainer, "empty container");
+ // Allow dropping into Tag containers and editable folders.
+ return !PlacesUtils.nodeIsTagQuery(aContainer) &&
+ (!PlacesUtils.nodeIsFolder(aContainer) ||
+ PlacesUIUtils.isContentsReadOnly(aContainer));
+ }
+};
+
+
+XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
+ "@mozilla.org/widget/dragservice;1",
+ "nsIDragService");
+
+function goUpdatePlacesCommands() {
+ // Get the controller for one of the places commands.
+ var placesController = doGetPlacesControllerForCommand("placesCmd_open");
+ function updatePlacesCommand(aCommand) {
+ goSetCommandEnabled(aCommand, placesController &&
+ placesController.isCommandEnabled(aCommand));
+ }
+
+ updatePlacesCommand("placesCmd_open");
+ updatePlacesCommand("placesCmd_open:window");
+ updatePlacesCommand("placesCmd_open:privatewindow");
+ updatePlacesCommand("placesCmd_open:tab");
+ updatePlacesCommand("placesCmd_new:folder");
+ updatePlacesCommand("placesCmd_new:bookmark");
+ updatePlacesCommand("placesCmd_new:separator");
+ updatePlacesCommand("placesCmd_show:info");
+ updatePlacesCommand("placesCmd_moveBookmarks");
+ updatePlacesCommand("placesCmd_reload");
+ updatePlacesCommand("placesCmd_sortBy:name");
+ updatePlacesCommand("placesCmd_cut");
+ updatePlacesCommand("placesCmd_copy");
+ updatePlacesCommand("placesCmd_paste");
+ updatePlacesCommand("placesCmd_delete");
+}
+
+function doGetPlacesControllerForCommand(aCommand)
+{
+ // A context menu may be built for non-focusable views. Thus, we first try
+ // to look for a view associated with document.popupNode
+ let popupNode;
+ try {
+ popupNode = document.popupNode;
+ } catch (e) {
+ // The document went away (bug 797307).
+ return null;
+ }
+ if (popupNode) {
+ let view = PlacesUIUtils.getViewForNode(popupNode);
+ if (view && view._contextMenuShown)
+ return view.controllers.getControllerForCommand(aCommand);
+ }
+
+ // When we're not building a context menu, only focusable views
+ // are possible. Thus, we can safely use the command dispatcher.
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand(aCommand);
+ if (controller)
+ return controller;
+
+ return null;
+}
+
+function goDoPlacesCommand(aCommand)
+{
+ let controller = doGetPlacesControllerForCommand(aCommand);
+ if (controller && controller.isCommandEnabled(aCommand))
+ controller.doCommand(aCommand);
+}
diff --git a/browser/components/places/content/downloadsViewOverlay.xul b/browser/components/places/content/downloadsViewOverlay.xul
new file mode 100644
index 000000000..5706632ba
--- /dev/null
+++ b/browser/components/places/content/downloadsViewOverlay.xul
@@ -0,0 +1,47 @@
+<!-- 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/. -->
+
+<?xul-overlay href="chrome://browser/content/downloads/allDownloadsViewOverlay.xul"?>
+
+<!DOCTYPE overlay [
+<!ENTITY % downloadsDTD SYSTEM "chrome://browser/locale/downloads/downloads.dtd">
+%downloadsDTD;
+]>
+
+<overlay id="downloadsViewOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript"><![CDATA[
+ const DOWNLOADS_QUERY = "place:transition=" +
+ Components.interfaces.nsINavHistoryService.TRANSITION_DOWNLOAD +
+ "&sort=" +
+ Components.interfaces.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING;
+
+ ContentArea.setContentViewForQueryString(DOWNLOADS_QUERY,
+ () => new DownloadsPlacesView(document.getElementById("downloadsRichListBox"), false),
+ { showDetailsPane: false,
+ toolbarSet: "back-button, forward-button, organizeButton, clearDownloadsButton, libraryToolbarSpacer, searchFilter" });
+ ]]></script>
+
+ <window id="places">
+ <commandset id="downloadCommands"/>
+ <menupopup id="downloadsContextMenu"/>
+ </window>
+
+ <deck id="placesViewsDeck">
+ <richlistbox id="downloadsRichListBox"/>
+ </deck>
+
+ <toolbar id="placesToolbar">
+ <toolbarbutton id="clearDownloadsButton"
+#ifdef XP_MACOSX
+ class="tabbable"
+#endif
+ insertbefore="libraryToolbarSpacer"
+ label="&clearDownloadsButton.label;"
+ command="downloadsCmd_clearDownloads"
+ tooltiptext="&clearDownloadsButton.tooltip;"/>
+ </toolbar>
+
+</overlay>
diff --git a/browser/components/places/content/editBookmarkOverlay.js b/browser/components/places/content/editBookmarkOverlay.js
new file mode 100644
index 000000000..e26cfb138
--- /dev/null
+++ b/browser/components/places/content/editBookmarkOverlay.js
@@ -0,0 +1,1168 @@
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed";
+const MAX_FOLDER_ITEM_IN_MENU_LIST = 5;
+
+var gEditItemOverlay = {
+ _observersAdded: false,
+ _staticFoldersListBuilt: false,
+
+ _paneInfo: null,
+ _setPaneInfo(aInitInfo) {
+ if (!aInitInfo)
+ return this._paneInfo = null;
+
+ if ("uris" in aInitInfo && "node" in aInitInfo)
+ throw new Error("ambiguous pane info");
+ if (!("uris" in aInitInfo) && !("node" in aInitInfo))
+ throw new Error("Neither node nor uris set for pane info");
+
+ let node = "node" in aInitInfo ? aInitInfo.node : null;
+
+ // Since there's no true UI for folder shortcuts (they show up just as their target
+ // folders), when the pane shows for them it's opened in read-only mode, showing the
+ // properties of the target folder.
+ let itemId = node ? node.itemId : -1;
+ let itemGuid = PlacesUIUtils.useAsyncTransactions && node ?
+ PlacesUtils.getConcreteItemGuid(node) : null;
+ let isItem = itemId != -1;
+ let isFolderShortcut = isItem &&
+ node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
+ let isTag = node && PlacesUtils.nodeIsTagQuery(node);
+ if (isTag) {
+ itemId = PlacesUtils.getConcreteItemId(node);
+ // For now we don't have access to the item guid synchronously for tags,
+ // so we'll need to fetch it later.
+ }
+ let isURI = node && PlacesUtils.nodeIsURI(node);
+ let uri = isURI ? NetUtil.newURI(node.uri) : null;
+ let title = node ? node.title : null;
+ let isBookmark = isItem && isURI;
+ let bulkTagging = !node;
+ let uris = bulkTagging ? aInitInfo.uris : null;
+ let visibleRows = new Set();
+ let isParentReadOnly = false;
+ let postData = aInitInfo.postData;
+ if (node && "parent" in node) {
+ let parent = node.parent;
+ if (parent) {
+ isParentReadOnly = !PlacesUtils.nodeIsFolder(parent) ||
+ PlacesUIUtils.isContentsReadOnly(parent);
+ }
+ }
+ let focusedElement = aInitInfo.focusedElement;
+
+ return this._paneInfo = { itemId, itemGuid, isItem,
+ isURI, uri, title,
+ isBookmark, isFolderShortcut, isParentReadOnly,
+ bulkTagging, uris,
+ visibleRows, postData, isTag, focusedElement };
+ },
+
+ get initialized() {
+ return this._paneInfo != null;
+ },
+
+ // Backwards-compatibility getters
+ get itemId() {
+ if (!this.initialized || this._paneInfo.bulkTagging)
+ return -1;
+ return this._paneInfo.itemId;
+ },
+
+ get uri() {
+ if (!this.initialized)
+ return null;
+ if (this._paneInfo.bulkTagging)
+ return this._paneInfo.uris[0];
+ return this._paneInfo.uri;
+ },
+
+ get multiEdit() {
+ return this.initialized && this._paneInfo.bulkTagging;
+ },
+
+ // Check if the pane is initialized to show only read-only fields.
+ get readOnly() {
+ // TODO (Bug 1120314): Folder shortcuts are currently read-only due to some
+ // quirky implementation details (the most important being the "smart"
+ // semantics of node.title that makes hard to edit the right entry).
+ // This pane is read-only if:
+ // * the panel is not initialized
+ // * the node is a folder shortcut
+ // * the node is not bookmarked and not a tag container
+ // * the node is child of a read-only container and is not a bookmarked
+ // URI nor a tag container
+ return !this.initialized ||
+ this._paneInfo.isFolderShortcut ||
+ (!this._paneInfo.isItem && !this._paneInfo.isTag) ||
+ (this._paneInfo.isParentReadOnly && !this._paneInfo.isBookmark && !this._paneInfo.isTag);
+ },
+
+ // the first field which was edited after this panel was initialized for
+ // a certain item
+ _firstEditedField: "",
+
+ _initNamePicker() {
+ if (this._paneInfo.bulkTagging)
+ throw new Error("_initNamePicker called unexpectedly");
+
+ // title may by null, which, for us, is the same as an empty string.
+ this._initTextField(this._namePicker, this._paneInfo.title || "");
+ },
+
+ _initLocationField() {
+ if (!this._paneInfo.isURI)
+ throw new Error("_initLocationField called unexpectedly");
+ this._initTextField(this._locationField, this._paneInfo.uri.spec);
+ },
+
+ _initDescriptionField() {
+ if (!this._paneInfo.isItem)
+ throw new Error("_initDescriptionField called unexpectedly");
+
+ this._initTextField(this._descriptionField,
+ PlacesUIUtils.getItemDescription(this._paneInfo.itemId));
+ },
+
+ _initKeywordField: Task.async(function* (newKeyword = "") {
+ if (!this._paneInfo.isBookmark) {
+ throw new Error("_initKeywordField called unexpectedly");
+ }
+
+ // Reset the field status synchronously now, eventually we'll reinit it
+ // later if we find an existing keyword. This way we can ensure to be in a
+ // consistent status when reusing the panel across different bookmarks.
+ this._keyword = newKeyword;
+ this._initTextField(this._keywordField, newKeyword);
+
+ if (!newKeyword) {
+ let entries = [];
+ yield PlacesUtils.keywords.fetch({ url: this._paneInfo.uri.spec },
+ e => entries.push(e));
+ if (entries.length > 0) {
+ // We show an existing keyword if either POST data was not provided, or
+ // if the POST data is the same.
+ let existingKeyword = entries[0].keyword;
+ let postData = this._paneInfo.postData;
+ if (postData) {
+ let sameEntry = entries.find(e => e.postData === postData);
+ existingKeyword = sameEntry ? sameEntry.keyword : "";
+ }
+ if (existingKeyword) {
+ this._keyword = existingKeyword;
+ // Update the text field to the existing keyword.
+ this._initTextField(this._keywordField, this._keyword);
+ }
+ }
+ }
+ }),
+
+ _initLoadInSidebar: Task.async(function* () {
+ if (!this._paneInfo.isBookmark)
+ throw new Error("_initLoadInSidebar called unexpectedly");
+
+ this._loadInSidebarCheckbox.checked =
+ PlacesUtils.annotations.itemHasAnnotation(
+ this._paneInfo.itemId, PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
+ }),
+
+ /**
+ * Initialize the panel.
+ *
+ * @param aInfo
+ * An object having:
+ * 1. one of the following properties:
+ * - node: either a result node or a node-like object representing the
+ * item to be edited. A node-like object must have the following
+ * properties (with values that match exactly those a result node
+ * would have): itemId, bookmarkGuid, uri, title, type.
+ * - uris: an array of uris for bulk tagging.
+ *
+ * 2. any of the following optional properties:
+ * - hiddenRows (Strings array): list of rows to be hidden regardless
+ * of the item edited. Possible values: "title", "location",
+ * "description", "keyword", "loadInSidebar", "feedLocation",
+ * "siteLocation", folderPicker"
+ */
+ initPanel(aInfo) {
+ if (typeof(aInfo) != "object" || aInfo === null)
+ throw new Error("aInfo must be an object.");
+ if ("node" in aInfo) {
+ try {
+ aInfo.node.type;
+ } catch (e) {
+ // If the lazy loader for |type| generates an exception, it means that
+ // this bookmark could not be loaded. This sometimes happens when tests
+ // create a bookmark by clicking the bookmark star, then try to cleanup
+ // before the bookmark panel has finished opening. Either way, if we
+ // cannot retrieve the bookmark information, we cannot open the panel.
+ return;
+ }
+ }
+
+ // 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);
+
+ let { itemId, isItem, isURI,
+ isBookmark, bulkTagging, uris,
+ visibleRows, focusedElement } = this._setPaneInfo(aInfo);
+
+ let showOrCollapse =
+ (rowId, isAppropriateForInput, nameInHiddenRows = null) => {
+ let visible = isAppropriateForInput;
+ if (visible && "hiddenRows" in aInfo && nameInHiddenRows)
+ visible &= aInfo.hiddenRows.indexOf(nameInHiddenRows) == -1;
+ if (visible)
+ visibleRows.add(rowId);
+ return !(this._element(rowId).collapsed = !visible);
+ };
+
+ if (showOrCollapse("nameRow", !bulkTagging, "name")) {
+ this._initNamePicker();
+ this._namePicker.readOnly = this.readOnly;
+ }
+
+ // In some cases we want to hide the location field, since it's not
+ // human-readable, but we still want to initialize it.
+ showOrCollapse("locationRow", isURI, "location");
+ if (isURI) {
+ this._initLocationField();
+ this._locationField.readOnly = this.readOnly;
+ }
+
+ // hide the description field for
+ if (showOrCollapse("descriptionRow", isItem && !this.readOnly,
+ "description")) {
+ this._initDescriptionField();
+ this._descriptionField.readOnly = this.readOnly;
+ }
+
+ if (showOrCollapse("keywordRow", isBookmark, "keyword")) {
+ this._initKeywordField().catch(Components.utils.reportError);
+ this._keywordField.readOnly = this.readOnly;
+ }
+
+ // Collapse the tag selector if the item does not accept tags.
+ if (showOrCollapse("tagsRow", isURI || bulkTagging, "tags"))
+ this._initTagsField().catch(Components.utils.reportError);
+ else if (!this._element("tagsSelectorRow").collapsed)
+ this.toggleTagsSelector().catch(Components.utils.reportError);
+
+ // Load in sidebar.
+ if (showOrCollapse("loadInSidebarCheckbox", isBookmark, "loadInSidebar")) {
+ this._initLoadInSidebar();
+ }
+
+ // Folder picker.
+ // Technically we should check that the item is not moveable, but that's
+ // not cheap (we don't always have the parent), and there's no use case for
+ // this (it's only the Star UI that shows the folderPicker)
+ if (showOrCollapse("folderRow", isItem, "folderPicker")) {
+ let containerId = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
+ this._initFolderMenuList(containerId);
+ }
+
+ // Selection count.
+ if (showOrCollapse("selectionCount", bulkTagging)) {
+ this._element("itemsCountText").value =
+ PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
+ uris.length,
+ [uris.length]);
+ }
+
+ // Observe changes.
+ if (!this._observersAdded) {
+ PlacesUtils.bookmarks.addObserver(this, false);
+ window.addEventListener("unload", this, false);
+ this._observersAdded = true;
+ }
+
+ // The focusedElement possible values are:
+ // * preferred: focus the field that the user touched first the last
+ // time the pane was shown (either namePicker or tagsField)
+ // * first: focus the first non collapsed textbox
+ // Note: since all controls are collapsed by default, we don't get the
+ // default XUL dialog behavior, that selects the first control, so we set
+ // the focus explicitly.
+ let elt;
+ if (focusedElement === "preferred") {
+ elt = this._element(gPrefService.getCharPref("browser.bookmarks.editDialog.firstEditField"));
+ } else if (focusedElement === "first") {
+ elt = document.querySelector("textbox:not([collapsed=true])");
+ }
+ if (elt) {
+ elt.focus();
+ elt.select();
+ }
+ },
+
+ /**
+ * Finds tags that are in common among this._currentInfo.uris;
+ */
+ _getCommonTags() {
+ if ("_cachedCommonTags" in this._paneInfo)
+ return this._paneInfo._cachedCommonTags;
+
+ let uris = [...this._paneInfo.uris];
+ let firstURI = uris.shift();
+ let commonTags = new Set(PlacesUtils.tagging.getTagsForURI(firstURI));
+ if (commonTags.size == 0)
+ return this._cachedCommonTags = [];
+
+ for (let uri of uris) {
+ let curentURITags = PlacesUtils.tagging.getTagsForURI(uri);
+ for (let tag of commonTags) {
+ if (!curentURITags.includes(tag)) {
+ commonTags.delete(tag)
+ if (commonTags.size == 0)
+ return this._paneInfo.cachedCommonTags = [];
+ }
+ }
+ }
+ return this._paneInfo._cachedCommonTags = [...commonTags];
+ },
+
+ _initTextField(aElement, aValue) {
+ if (aElement.value != aValue) {
+ aElement.value = aValue;
+
+ // Clear the undo stack
+ let editor = aElement.editor;
+ if (editor)
+ editor.transactionManager.clear();
+ }
+ },
+
+ /**
+ * 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(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 (let 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 (let 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:
+ XPCOMUtils.generateQI([Components.interfaces.nsIDOMEventListener,
+ Components.interfaces.nsINavBookmarkObserver]),
+
+ _element(aID) {
+ return document.getElementById("editBMPanel_" + aID);
+ },
+
+ 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) {
+ PlacesUtils.bookmarks.removeObserver(this);
+ this._observersAdded = false;
+ }
+
+ this._setPaneInfo(null);
+ this._firstEditedField = "";
+ },
+
+ onTagsFieldChange() {
+ if (this._paneInfo.isURI || this._paneInfo.bulkTagging) {
+ this._updateTags().then(
+ anyChanges => {
+ if (anyChanges)
+ this._mayUpdateFirstEditField("tagsField");
+ }, Components.utils.reportError);
+ }
+ },
+
+ /**
+ * For a given array of currently-set tags and the tags-input-field
+ * value, returns which tags should be removed and which should be added in
+ * the form of { removedTags: [...], newTags: [...] }.
+ */
+ _getTagsChanges(aCurrentTags) {
+ let inputTags = this._getTagsArrayFromTagsInputField();
+
+ // Optimize the trivial cases (which are actually the most common).
+ if (inputTags.length == 0 && aCurrentTags.length == 0)
+ return { newTags: [], removedTags: [] };
+ if (inputTags.length == 0)
+ return { newTags: [], removedTags: aCurrentTags };
+ if (aCurrentTags.length == 0)
+ return { newTags: inputTags, removedTags: [] };
+
+ let removedTags = aCurrentTags.filter(t => !inputTags.includes(t));
+ let newTags = inputTags.filter(t => !aCurrentTags.includes(t));
+ return { removedTags, newTags };
+ },
+
+ // Adds and removes tags for one or more uris.
+ _setTagsFromTagsInputField: Task.async(function* (aCurrentTags, aURIs) {
+ let { removedTags, newTags } = this._getTagsChanges(aCurrentTags);
+ if (removedTags.length + newTags.length == 0)
+ return false;
+
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let txns = [];
+ for (let uri of aURIs) {
+ if (removedTags.length > 0)
+ txns.push(new PlacesUntagURITransaction(uri, removedTags));
+ if (newTags.length > 0)
+ txns.push(new PlacesTagURITransaction(uri, newTags));
+ }
+
+ PlacesUtils.transactionManager.doTransaction(
+ new PlacesAggregatedTransaction("Update tags", txns));
+ return true;
+ }
+
+ let setTags = function* () {
+ if (newTags.length > 0) {
+ yield PlacesTransactions.Tag({ urls: aURIs, tags: newTags })
+ .transact();
+ }
+ if (removedTags.length > 0) {
+ yield PlacesTransactions.Untag({ urls: aURIs, tags: removedTags })
+ .transact();
+ }
+ };
+
+ // Only in the library info-pane it's safe (and necessary) to batch these.
+ // TODO bug 1093030: cleanup this mess when the bookmarksProperties dialog
+ // and star UI code don't "run a batch in the background".
+ if (window.document.documentElement.id == "places")
+ PlacesTransactions.batch(setTags).catch(Components.utils.reportError);
+ else
+ Task.spawn(setTags).catch(Components.utils.reportError);
+ return true;
+ }),
+
+ _updateTags: Task.async(function*() {
+ let uris = this._paneInfo.bulkTagging ?
+ this._paneInfo.uris : [this._paneInfo.uri];
+ let currentTags = this._paneInfo.bulkTagging ?
+ yield this._getCommonTags() :
+ PlacesUtils.tagging.getTagsForURI(uris[0]);
+ let anyChanges = yield this._setTagsFromTagsInputField(currentTags, uris);
+ if (!anyChanges)
+ return false;
+
+ // The panel could have been closed in the meanwhile.
+ if (!this._paneInfo)
+ return false;
+
+ // Ensure the tagsField is in sync, clean it up from empty tags
+ currentTags = this._paneInfo.bulkTagging ?
+ this._getCommonTags() :
+ PlacesUtils.tagging.getTagsForURI(this._paneInfo.uri);
+ this._initTextField(this._tagsField, currentTags.join(", "), false);
+ return true;
+ }),
+
+ /**
+ * 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(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._paneInfo.bulkTagging || 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);
+ },
+
+ onNamePickerChange() {
+ if (this.readOnly || !(this._paneInfo.isItem || this._paneInfo.isTag))
+ return;
+
+ // Here we update either the item title or its cached static title
+ let newTitle = this._namePicker.value;
+ if (!newTitle &&
+ PlacesUtils.bookmarks.getFolderIdForItem(this._paneInfo.itemId) == PlacesUtils.tagsFolderId) {
+ // We don't allow setting an empty title for a tag, restore the old one.
+ this._initNamePicker();
+ }
+ else {
+ this._mayUpdateFirstEditField("namePicker");
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let txn = new PlacesEditItemTitleTransaction(this._paneInfo.itemId,
+ newTitle);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ return;
+ }
+ Task.spawn(function* () {
+ let guid = this._paneInfo.isTag
+ ? (yield PlacesUtils.promiseItemGuid(this._paneInfo.itemId))
+ : this._paneInfo.itemGuid;
+ PlacesTransactions.EditTitle({ guid, title: newTitle })
+ .transact().catch(Components.utils.reportError);
+ }).catch(Components.utils.reportError);
+ }
+ },
+
+ onDescriptionFieldChange() {
+ if (this.readOnly || !this._paneInfo.isItem)
+ return;
+
+ let itemId = this._paneInfo.itemId;
+ let description = this._element("descriptionField").value;
+ if (description != PlacesUIUtils.getItemDescription(this._paneInfo.itemId)) {
+ let annotation =
+ { name: PlacesUIUtils.DESCRIPTION_ANNO, value: description };
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let txn = new PlacesSetItemAnnotationTransaction(itemId,
+ annotation);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ return;
+ }
+ let guid = this._paneInfo.itemGuid;
+ PlacesTransactions.Annotate({ guid, annotation })
+ .transact().catch(Components.utils.reportError);
+ }
+ },
+
+ onLocationFieldChange() {
+ if (this.readOnly || !this._paneInfo.isBookmark)
+ return;
+
+ let newURI;
+ try {
+ newURI = PlacesUIUtils.createFixedURI(this._locationField.value);
+ }
+ catch (ex) {
+ // TODO: Bug 1089141 - Provide some feedback about the invalid url.
+ return;
+ }
+
+ if (this._paneInfo.uri.equals(newURI))
+ return;
+
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let txn = new PlacesEditBookmarkURITransaction(this._paneInfo.itemId, newURI);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ return;
+ }
+ let guid = this._paneInfo.itemGuid;
+ PlacesTransactions.EditUrl({ guid, url: newURI })
+ .transact().catch(Components.utils.reportError);
+ },
+
+ onKeywordFieldChange() {
+ if (this.readOnly || !this._paneInfo.isBookmark)
+ return;
+
+ let itemId = this._paneInfo.itemId;
+ let oldKeyword = this._keyword;
+ let keyword = this._keyword = this._keywordField.value;
+ let postData = this._paneInfo.postData;
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let txn = new PlacesEditBookmarkKeywordTransaction(itemId,
+ keyword,
+ postData,
+ oldKeyword);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ return;
+ }
+ let guid = this._paneInfo.itemGuid;
+ PlacesTransactions.EditKeyword({ guid, keyword, postData, oldKeyword })
+ .transact().catch(Components.utils.reportError);
+ },
+
+ onLoadInSidebarCheckboxCommand() {
+ if (!this.initialized || !this._paneInfo.isBookmark)
+ return;
+
+ let annotation = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO };
+ if (this._loadInSidebarCheckbox.checked)
+ annotation.value = true;
+
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let itemId = this._paneInfo.itemId;
+ let txn = new PlacesSetItemAnnotationTransaction(itemId,
+ annotation);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ return;
+ }
+ let guid = this._paneInfo.itemGuid;
+ PlacesTransactions.Annotate({ guid, annotation })
+ .transact().catch(Components.utils.reportError);
+ },
+
+ 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() {
+ 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(aFolderId) {
+ let menupopup = this._folderMenuList.menupopup;
+ let menuItem = Array.prototype.find.call(
+ menupopup.childNodes, menuItem => menuItem.folderId === aFolderId);
+ if (menuItem !== undefined)
+ return menuItem;
+
+ // 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(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
+ let containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._paneInfo.itemId);
+ let item = this._getFolderMenuItem(containerId);
+ this._folderMenuList.selectedItem = item;
+ // XXXmano HACK: setTimeout 100, otherwise focus goes back to the
+ // menulist right away
+ setTimeout(() => this.toggleFolderTreeVisibility(), 100);
+ return;
+ }
+
+ // Move the item
+ let containerId = this._getFolderIdFromMenuList();
+ if (PlacesUtils.bookmarks.getFolderIdForItem(this._paneInfo.itemId) != containerId &&
+ this._paneInfo.itemId != containerId) {
+ if (PlacesUIUtils.useAsyncTransactions) {
+ Task.spawn(function* () {
+ let newParentGuid = yield PlacesUtils.promiseItemGuid(containerId);
+ let guid = this._paneInfo.itemGuid;
+ yield PlacesTransactions.Move({ guid, newParentGuid }).transact();
+ }.bind(this));
+ }
+ else {
+ let txn = new PlacesMoveItemTransaction(this._paneInfo.itemId,
+ containerId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ }
+
+ // Mark the containing folder as recently-used if it isn't in the
+ // static list
+ if (containerId != PlacesUtils.unfiledBookmarksFolderId &&
+ containerId != PlacesUtils.toolbarFolderId &&
+ containerId != PlacesUtils.bookmarksMenuFolderId) {
+ this._markFolderAsRecentlyUsed(containerId)
+ .catch(Components.utils.reportError);
+ }
+
+ // Auto-show the bookmarks toolbar when adding / moving an item there.
+ if (containerId == PlacesUtils.toolbarFolderId) {
+ Services.obs.notifyObservers(null, "autoshow-bookmarks-toolbar", null);
+ }
+ }
+
+ // Update folder-tree selection
+ var folderTreeRow = this._element("folderTreeRow");
+ if (!folderTreeRow.collapsed) {
+ var selectedNode = this._folderTree.selectedNode;
+ if (!selectedNode ||
+ PlacesUtils.getConcreteItemId(selectedNode) != containerId)
+ this._folderTree.selectItems([containerId]);
+ }
+ },
+
+ 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: Task.async(function* (aFolderId) {
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let txns = [];
+
+ // Expire old unused recent folders.
+ let annotation = this._getLastUsedAnnotationObject(false);
+ while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) {
+ let folderId = this._recentFolders.pop().folderId;
+ let annoTxn = new PlacesSetItemAnnotationTransaction(folderId,
+ annotation);
+ txns.push(annoTxn);
+ }
+
+ // Mark folder as recently used
+ annotation = this._getLastUsedAnnotationObject(true);
+ let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId,
+ annotation);
+ txns.push(annoTxn);
+
+ let aggregate =
+ new PlacesAggregatedTransaction("Update last used folders", txns);
+ PlacesUtils.transactionManager.doTransaction(aggregate);
+ return;
+ }
+
+ // Expire old unused recent folders.
+ let guids = [];
+ while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) {
+ let folderId = this._recentFolders.pop().folderId;
+ let guid = yield PlacesUtils.promiseItemGuid(folderId);
+ guids.push(guid);
+ }
+ if (guids.length > 0) {
+ let annotation = this._getLastUsedAnnotationObject(false);
+ PlacesTransactions.Annotate({ guids, annotation })
+ .transact().catch(Components.utils.reportError);
+ }
+
+ // Mark folder as recently used
+ let annotation = this._getLastUsedAnnotationObject(true);
+ let guid = yield PlacesUtils.promiseItemGuid(aFolderId);
+ PlacesTransactions.Annotate({ guid, annotation })
+ .transact().catch(Components.utils.reportError);
+ }),
+
+ /**
+ * 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(aLastUsed) {
+ return { name: LAST_USED_ANNO,
+ value: aLastUsed ? new Date().getTime() : null };
+ },
+
+ _rebuildTagsSelectorList: Task.async(function* () {
+ let tagsSelector = this._element("tagsSelector");
+ let 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);
+ }
+
+ let tagsInField = this._getTagsArrayFromTagsInputField();
+ let allTags = PlacesUtils.tagging.allTags;
+ for (tag of allTags) {
+ let elt = document.createElement("listitem");
+ elt.setAttribute("type", "checkbox");
+ elt.setAttribute("label", tag);
+ if (tagsInField.includes(tag))
+ 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: Task.async(function* () {
+ 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;
+ yield 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.
+ */
+ _getTagsArrayFromTagsInputField() {
+ let tags = this._element("tagsField").value;
+ return tags.trim()
+ .split(/\s*,\s*/) // Split on commas and remove spaces.
+ .filter(tag => tag.length > 0); // Kill empty tags.
+ },
+
+ newFolder: Task.async(function* () {
+ let 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...
+ let title = this._element("newFolderButton").label;
+ if (PlacesUIUtils.useAsyncTransactions) {
+ let parentGuid = yield ip.promiseGuid();
+ yield PlacesTransactions.NewFolder({ parentGuid, title, index: ip.index })
+ .transact().catch(Components.utils.reportError);
+ }
+ else {
+ let txn = new PlacesCreateFolderTransaction(title, ip.itemId, ip.index);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ }
+
+ this._folderTree.focus();
+ this._folderTree.selectItems([ip.itemId]);
+ PlacesUtils.asContainer(this._folderTree.selectedNode).containerOpen = true;
+ this._folderTree.selectItems([this._lastNewItem]);
+ this._folderTree.startEditing(this._folderTree.view.selection.currentIndex,
+ this._folderTree.columns.getFirstColumn());
+ }),
+
+ // nsIDOMEventListener
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "CheckboxStateChange":
+ // Update the tags field when items are checked/unchecked in the listbox
+ let tags = this._getTagsArrayFromTagsInputField();
+ let tagCheckbox = aEvent.target;
+
+ let curTagIndex = tags.indexOf(tagCheckbox.label);
+ let tagsSelector = this._element("tagsSelector");
+ tagsSelector.selectedItem = tagCheckbox;
+
+ if (tagCheckbox.checked) {
+ if (curTagIndex == -1)
+ tags.push(tagCheckbox.label);
+ }
+ else if (curTagIndex != -1) {
+ tags.splice(curTagIndex, 1);
+ }
+ this._element("tagsField").value = tags.join(", ");
+ this._updateTags();
+ break;
+ case "unload":
+ this.uninitPanel(false);
+ break;
+ }
+ },
+
+ _initTagsField: Task.async(function* () {
+ let tags;
+ if (this._paneInfo.isURI)
+ tags = PlacesUtils.tagging.getTagsForURI(this._paneInfo.uri);
+ else if (this._paneInfo.bulkTagging)
+ tags = this._getCommonTags();
+ else
+ throw new Error("_promiseTagsStr called unexpectedly");
+
+ this._initTextField(this._tagsField, tags.join(", "));
+ }),
+
+ _onTagsChange(aItemId) {
+ let paneInfo = this._paneInfo;
+ let updateTagsField = false;
+ if (paneInfo.isURI) {
+ if (paneInfo.isBookmark && aItemId == paneInfo.itemId) {
+ updateTagsField = true;
+ }
+ else if (!paneInfo.isBookmark) {
+ let changedURI = PlacesUtils.bookmarks.getBookmarkURI(aItemId);
+ updateTagsField = changedURI.equals(paneInfo.uri);
+ }
+ }
+ else if (paneInfo.bulkTagging) {
+ let changedURI = PlacesUtils.bookmarks.getBookmarkURI(aItemId);
+ if (paneInfo.uris.some(uri => uri.equals(changedURI))) {
+ updateTagsField = true;
+ delete this._paneInfo._cachedCommonTags;
+ }
+ }
+ else {
+ throw new Error("_onTagsChange called unexpectedly");
+ }
+
+ if (updateTagsField)
+ this._initTagsField().catch(Components.utils.reportError);
+
+ // Any tags change should be reflected in the tags selector.
+ if (this._element("tagsSelector"))
+ this._rebuildTagsSelectorList().catch(Components.utils.reportError);
+ },
+
+ _onItemTitleChange(aItemId, aNewTitle) {
+ if (!this._paneInfo.isBookmark)
+ return;
+ if (aItemId == this._paneInfo.itemId) {
+ this._paneInfo.title = aNewTitle;
+ this._initTextField(this._namePicker, aNewTitle);
+ }
+ else if (this._paneInfo.visibleRows.has("folderRow")) {
+ // 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.
+ let menupopup = this._folderMenuList.menupopup;
+ for (menuitem of menupopup.childNodes) {
+ if ("folderId" in menuitem && menuitem.folderId == aItemId) {
+ menuitem.label = aNewTitle;
+ break;
+ }
+ }
+ }
+ },
+
+ // nsINavBookmarkObserver
+ onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aValue,
+ aLastModified, aItemType) {
+ if (aProperty == "tags" && this._paneInfo.visibleRows.has("tagsRow")) {
+ this._onTagsChange(aItemId);
+ }
+ else if (aProperty == "title" && this._paneInfo.isItem) {
+ // This also updates titles of folders in the folder menu list.
+ this._onItemTitleChange(aItemId, aValue);
+ }
+ else if (!this._paneInfo.isItem || this._paneInfo.itemId != aItemId) {
+ return;
+ }
+
+ switch (aProperty) {
+ case "uri":
+ let newURI = NetUtil.newURI(aValue);
+ if (!newURI.equals(this._paneInfo.uri)) {
+ this._paneInfo.uri = newURI;
+ if (this._paneInfo.visibleRows.has("locationRow"))
+ this._initLocationField();
+
+ if (this._paneInfo.visibleRows.has("tagsRow")) {
+ delete this._paneInfo._cachedCommonTags;
+ this._onTagsChange(aItemId);
+ }
+ }
+ break;
+ case "keyword":
+ if (this._paneInfo.visibleRows.has("keywordRow"))
+ this._initKeywordField(aValue).catch(Components.utils.reportError);
+ break;
+ case PlacesUIUtils.DESCRIPTION_ANNO:
+ if (this._paneInfo.visibleRows.has("descriptionRow"))
+ this._initDescriptionField();
+ break;
+ case PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO:
+ if (this._paneInfo.visibleRows.has("loadInSidebarCheckbox"))
+ this._initLoadInSidebar();
+ break;
+ }
+ },
+
+ onItemMoved(aItemId, aOldParent, aOldIndex,
+ aNewParent, aNewIndex, aItemType) {
+ if (!this._paneInfo.isItem ||
+ !this._paneInfo.visibleRows.has("folderPicker") ||
+ this._paneInfo.itemId != aItemOd ||
+ aNewParent == this._getFolderIdFromMenuList()) {
+ return;
+ }
+
+ // Just setting selectItem _does not_ trigger oncommand, so we don't
+ // recurse.
+ this._folderMenuList.selectedItem = this._getFolderMenuItem(aNewParent);
+ },
+
+ onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI) {
+ this._lastNewItem = aItemId;
+ },
+
+ onItemRemoved() { },
+ onBeginUpdateBatch() { },
+ onEndUpdateBatch() { },
+ onItemVisited() { },
+};
+
+
+for (let elt of ["folderMenuList", "folderTree", "namePicker",
+ "locationField", "descriptionField", "keywordField",
+ "tagsField", "loadInSidebarCheckbox"]) {
+ let eltScoped = elt;
+ XPCOMUtils.defineLazyGetter(gEditItemOverlay, `_${eltScoped}`,
+ () => gEditItemOverlay._element(eltScoped));
+}
diff --git a/browser/components/places/content/editBookmarkOverlay.xul b/browser/components/places/content/editBookmarkOverlay.xul
new file mode 100644
index 000000000..140e752c0
--- /dev/null
+++ b/browser/components/places/content/editBookmarkOverlay.xul
@@ -0,0 +1,188 @@
+<!-- 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/. -->
+
+<!DOCTYPE overlay [
+<!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
+%editBookmarkOverlayDTD;
+]>
+
+<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<overlay id="editBookmarkOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <vbox id="editBookmarkPanelContent" flex="1">
+ <hbox id="editBMPanel_selectionCount" pack="center">
+ <label id="editBMPanel_itemsCountText"/>
+ </hbox>
+
+ <grid id="editBookmarkPanelGrid" flex="1">
+ <columns id="editBMPanel_columns">
+ <column id="editBMPanel_labelColumn" />
+ <column flex="1" id="editBMPanel_editColumn" />
+ </columns>
+ <rows id="editBMPanel_rows">
+ <row id="editBMPanel_nameRow"
+ align="center"
+ collapsed="true">
+ <label value="&editBookmarkOverlay.name.label;"
+ class="editBMPanel_rowLabel"
+ accesskey="&editBookmarkOverlay.name.accesskey;"
+ control="editBMPanel_namePicker"/>
+ <textbox id="editBMPanel_namePicker"
+ onchange="gEditItemOverlay.onNamePickerChange();"/>
+ </row>
+
+ <row id="editBMPanel_locationRow"
+ align="center"
+ collapsed="true">
+ <label value="&editBookmarkOverlay.location.label;"
+ class="editBMPanel_rowLabel"
+ accesskey="&editBookmarkOverlay.location.accesskey;"
+ control="editBMPanel_locationField"/>
+ <textbox id="editBMPanel_locationField"
+ class="uri-element"
+ onchange="gEditItemOverlay.onLocationFieldChange();"/>
+ </row>
+
+ <row id="editBMPanel_folderRow"
+ align="center"
+ collapsed="true">
+ <label value="&editBookmarkOverlay.folder.label;"
+ class="editBMPanel_rowLabel"
+ control="editBMPanel_folderMenuList"/>
+ <hbox flex="1" align="center">
+ <menulist id="editBMPanel_folderMenuList"
+ class="folder-icon"
+ flex="1"
+ oncommand="gEditItemOverlay.onFolderMenuListCommand(event);">
+ <menupopup>
+ <!-- Static item for special folders -->
+ <menuitem id="editBMPanel_toolbarFolderItem"
+ class="menuitem-iconic folder-icon"/>
+ <menuitem id="editBMPanel_bmRootItem"
+ class="menuitem-iconic folder-icon"/>
+ <menuitem id="editBMPanel_unfiledRootItem"
+ class="menuitem-iconic folder-icon"/>
+ <menuseparator id="editBMPanel_chooseFolderSeparator"/>
+ <menuitem id="editBMPanel_chooseFolderMenuItem"
+ label="&editBookmarkOverlay.choose.label;"
+ class="menuitem-iconic folder-icon"/>
+ <menuseparator id="editBMPanel_foldersSeparator" hidden="true"/>
+ </menupopup>
+ </menulist>
+ <button id="editBMPanel_foldersExpander"
+ class="expander-down"
+ tooltiptext="&editBookmarkOverlay.foldersExpanderDown.tooltip;"
+ tooltiptextdown="&editBookmarkOverlay.foldersExpanderDown.tooltip;"
+ tooltiptextup="&editBookmarkOverlay.expanderUp.tooltip;"
+ oncommand="gEditItemOverlay.toggleFolderTreeVisibility();"/>
+ </hbox>
+ </row>
+
+ <row id="editBMPanel_folderTreeRow"
+ collapsed="true"
+ flex="1">
+ <spacer/>
+ <vbox flex="1">
+ <tree id="editBMPanel_folderTree"
+ flex="1"
+ class="placesTree"
+ type="places"
+ height="150"
+ minheight="150"
+ editable="true"
+ onselect="gEditItemOverlay.onFolderTreeSelect();"
+ hidecolumnpicker="true">
+ <treecols>
+ <treecol anonid="title" flex="1" primary="true" hideheader="true"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <hbox id="editBMPanel_newFolderBox">
+ <button label="&editBookmarkOverlay.newFolderButton.label;"
+ id="editBMPanel_newFolderButton"
+ accesskey="&editBookmarkOverlay.newFolderButton.accesskey;"
+ oncommand="gEditItemOverlay.newFolder().catch(Components.utils.reportError);"/>
+ </hbox>
+ </vbox>
+ </row>
+
+ <row id="editBMPanel_tagsRow"
+ align="center"
+ collapsed="true">
+ <label value="&editBookmarkOverlay.tags.label;"
+ class="editBMPanel_rowLabel"
+ accesskey="&editBookmarkOverlay.tags.accesskey;"
+ control="editBMPanel_tagsField"/>
+ <hbox flex="1" align="center">
+ <textbox id="editBMPanel_tagsField"
+ type="autocomplete"
+ class="padded"
+ flex="1"
+ autocompletesearch="places-tag-autocomplete"
+ completedefaultindex="true"
+ tabscrolling="true"
+ showcommentcolumn="true"
+ placeholder="&editBookmarkOverlay.tagsEmptyDesc.label;"
+ onchange="gEditItemOverlay.onTagsFieldChange();"/>
+ <button id="editBMPanel_tagsSelectorExpander"
+ class="expander-down"
+ tooltiptext="&editBookmarkOverlay.tagsExpanderDown.tooltip;"
+ tooltiptextdown="&editBookmarkOverlay.tagsExpanderDown.tooltip;"
+ tooltiptextup="&editBookmarkOverlay.expanderUp.tooltip;"
+ oncommand="gEditItemOverlay.toggleTagsSelector();"/>
+ </hbox>
+ </row>
+
+ <row id="editBMPanel_tagsSelectorRow"
+ align="center"
+ collapsed="true">
+ <spacer/>
+ <listbox id="editBMPanel_tagsSelector"
+ height="150"/>
+ </row>
+
+ <row id="editBMPanel_keywordRow"
+ align="center"
+ collapsed="true">
+ <observes element="additionalInfoBroadcaster" attribute="hidden"/>
+ <label value="&editBookmarkOverlay.keyword.label;"
+ class="editBMPanel_rowLabel"
+ accesskey="&editBookmarkOverlay.keyword.accesskey;"
+ control="editBMPanel_keywordField"/>
+ <textbox id="editBMPanel_keywordField"
+ onchange="gEditItemOverlay.onKeywordFieldChange();"/>
+ </row>
+
+ <row id="editBMPanel_descriptionRow"
+ collapsed="true">
+ <observes element="additionalInfoBroadcaster" attribute="hidden"/>
+ <label value="&editBookmarkOverlay.description.label;"
+ class="editBMPanel_rowLabel"
+ accesskey="&editBookmarkOverlay.description.accesskey;"
+ control="editBMPanel_descriptionField"/>
+ <textbox id="editBMPanel_descriptionField"
+ multiline="true"
+ rows="4"
+ onchange="gEditItemOverlay.onDescriptionFieldChange();"/>
+ </row>
+ </rows>
+ </grid>
+
+ <checkbox id="editBMPanel_loadInSidebarCheckbox"
+ collapsed="true"
+ label="&editBookmarkOverlay.loadInSidebar.label;"
+ accesskey="&editBookmarkOverlay.loadInSidebar.accesskey;"
+ oncommand="gEditItemOverlay.onLoadInSidebarCheckboxCommand();">
+ <observes element="additionalInfoBroadcaster" attribute="hidden"/>
+ </checkbox>
+
+ <!-- If the ids are changing or additional fields are being added, be sure
+ to sync the values in places.js -->
+ <broadcaster id="additionalInfoBroadcaster"/>
+ </vbox>
+</overlay>
diff --git a/browser/components/places/content/history-panel.js b/browser/components/places/content/history-panel.js
new file mode 100644
index 000000000..20dbbb5bd
--- /dev/null
+++ b/browser/components/places/content/history-panel.js
@@ -0,0 +1,98 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/TelemetryStopwatch.jsm");
+
+var gHistoryTree;
+var gSearchBox;
+var gHistoryGrouping = "";
+var gSearching = false;
+
+function HistorySidebarInit()
+{
+ gHistoryTree = document.getElementById("historyTree");
+ gSearchBox = document.getElementById("search-box");
+
+ gHistoryGrouping = document.getElementById("viewButton").
+ getAttribute("selectedsort");
+
+ if (gHistoryGrouping == "site")
+ document.getElementById("bysite").setAttribute("checked", "true");
+ else if (gHistoryGrouping == "visited")
+ document.getElementById("byvisited").setAttribute("checked", "true");
+ else if (gHistoryGrouping == "lastvisited")
+ document.getElementById("bylastvisited").setAttribute("checked", "true");
+ else if (gHistoryGrouping == "dayandsite")
+ document.getElementById("bydayandsite").setAttribute("checked", "true");
+ else
+ document.getElementById("byday").setAttribute("checked", "true");
+
+ searchHistory("");
+}
+
+function GroupBy(groupingType)
+{
+ gHistoryGrouping = groupingType;
+ searchHistory(gSearchBox.value);
+}
+
+function searchHistory(aInput)
+{
+ var query = PlacesUtils.history.getNewQuery();
+ var options = PlacesUtils.history.getNewQueryOptions();
+
+ const NHQO = Ci.nsINavHistoryQueryOptions;
+ var sortingMode;
+ var resultType;
+
+ switch (gHistoryGrouping) {
+ case "visited":
+ resultType = NHQO.RESULTS_AS_URI;
+ sortingMode = NHQO.SORT_BY_VISITCOUNT_DESCENDING;
+ break;
+ case "lastvisited":
+ resultType = NHQO.RESULTS_AS_URI;
+ sortingMode = NHQO.SORT_BY_DATE_DESCENDING;
+ break;
+ case "dayandsite":
+ resultType = NHQO.RESULTS_AS_DATE_SITE_QUERY;
+ break;
+ case "site":
+ resultType = NHQO.RESULTS_AS_SITE_QUERY;
+ sortingMode = NHQO.SORT_BY_TITLE_ASCENDING;
+ break;
+ case "day":
+ default:
+ resultType = NHQO.RESULTS_AS_DATE_QUERY;
+ break;
+ }
+
+ if (aInput) {
+ query.searchTerms = aInput;
+ if (gHistoryGrouping != "visited" && gHistoryGrouping != "lastvisited") {
+ sortingMode = NHQO.SORT_BY_FRECENCY_DESCENDING;
+ resultType = NHQO.RESULTS_AS_URI;
+ }
+ }
+
+ options.sortingMode = sortingMode;
+ options.resultType = resultType;
+ options.includeHidden = !!aInput;
+
+ if (gHistoryGrouping == "lastvisited")
+ this.TelemetryStopwatch.start("HISTORY_LASTVISITED_TREE_QUERY_TIME_MS");
+
+ // call load() on the tree manually
+ // instead of setting the place attribute in history-panel.xul
+ // otherwise, we will end up calling load() twice
+ gHistoryTree.load([query], options);
+
+ if (gHistoryGrouping == "lastvisited")
+ this.TelemetryStopwatch.finish("HISTORY_LASTVISITED_TREE_QUERY_TIME_MS");
+}
+
+window.addEventListener("SidebarFocused",
+ () => gSearchBox.focus(),
+ false);
diff --git a/browser/components/places/content/history-panel.xul b/browser/components/places/content/history-panel.xul
new file mode 100644
index 000000000..d1c875a63
--- /dev/null
+++ b/browser/components/places/content/history-panel.xul
@@ -0,0 +1,95 @@
+<?xml version="1.0"?> <!-- -*- Mode: xml; indent-tabs-mode: nil; -*- -->
+
+# 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/.
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<!DOCTYPE page [
+<!ENTITY % placesDTD SYSTEM "chrome://browser/locale/places/places.dtd">
+%placesDTD;
+]>
+
+<!-- we need to keep id="history-panel" for upgrade and switching
+ between versions of the browser -->
+
+<page id="history-panel" orient="vertical"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="HistorySidebarInit();"
+ onunload="SidebarUtils.setMouseoverURL('');">
+
+ <script type="application/javascript"
+ src="chrome://browser/content/bookmarks/sidebarUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/places/history-panel.js"/>
+
+ <commandset id="editMenuCommands"/>
+ <commandset id="placesCommands"/>
+
+ <keyset id="editMenuKeys">
+#ifdef XP_MACOSX
+ <key id="key_delete2" keycode="VK_BACK" command="cmd_delete"/>
+#endif
+ </keyset>
+
+ <!-- required to overlay the context menu -->
+ <menupopup id="placesContext"/>
+
+ <!-- Bookmarks and history tooltip -->
+ <tooltip id="bhTooltip"/>
+
+ <hbox id="sidebar-search-container" align="center">
+ <label id="sidebar-search-label"
+ value="&find.label;" accesskey="&find.accesskey;"
+ control="search-box"/>
+ <textbox id="search-box" flex="1" type="search" class="compact"
+ aria-controls="historyTree"
+ oncommand="searchHistory(this.value);"/>
+ <button id="viewButton" style="min-width:0px !important;" type="menu"
+ label="&view.label;" accesskey="&view.accesskey;" selectedsort="day"
+ persist="selectedsort">
+ <menupopup>
+ <menuitem id="bydayandsite" label="&byDayAndSite.label;"
+ accesskey="&byDayAndSite.accesskey;" type="radio"
+ oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'dayandsite'); GroupBy('dayandsite');"/>
+ <menuitem id="bysite" label="&bySite.label;"
+ accesskey="&bySite.accesskey;" type="radio"
+ oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'site'); GroupBy('site');"/>
+ <menuitem id="byday" label="&byDate.label;"
+ accesskey="&byDate.accesskey;"
+ type="radio"
+ oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'day'); GroupBy('day');"/>
+ <menuitem id="byvisited" label="&byMostVisited.label;"
+ accesskey="&byMostVisited.accesskey;"
+ type="radio"
+ oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'visited'); GroupBy('visited');"/>
+ <menuitem id="bylastvisited" label="&byLastVisited.label;"
+ accesskey="&byLastVisited.accesskey;"
+ type="radio"
+ oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'lastvisited'); GroupBy('lastvisited');"/>
+ </menupopup>
+ </button>
+ </hbox>
+
+ <tree id="historyTree"
+ class="sidebar-placesTree"
+ flex="1"
+ type="places"
+ context="placesContext"
+ hidecolumnpicker="true"
+ onkeypress="SidebarUtils.handleTreeKeyPress(event);"
+ onclick="SidebarUtils.handleTreeClick(this, event, true);"
+ onmousemove="SidebarUtils.handleTreeMouseMove(event);"
+ onmouseout="SidebarUtils.setMouseoverURL('');">
+ <treecols>
+ <treecol id="title" flex="1" primary="true" hideheader="true"/>
+ </treecols>
+ <treechildren class="sidebar-placesTreechildren" flex="1" tooltip="bhTooltip"/>
+ </tree>
+</page>
diff --git a/browser/components/places/content/menu.xml b/browser/components/places/content/menu.xml
new file mode 100644
index 000000000..f791d76fb
--- /dev/null
+++ b/browser/components/places/content/menu.xml
@@ -0,0 +1,633 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<bindings id="placesMenuBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="places-popup-base"
+ extends="chrome://global/content/bindings/popup.xml#popup">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox class="menupopup-drop-indicator-bar" hidden="true">
+ <xul:image class="menupopup-drop-indicator" mousethrough="always"/>
+ </xul:vbox>
+ <xul:arrowscrollbox class="popup-internal-box" flex="1" orient="vertical"
+ smoothscroll="false">
+ <children/>
+ </xul:arrowscrollbox>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+
+ <field name="AppConstants" readonly="true">
+ (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
+ </field>
+
+ <field name="_indicatorBar">
+ document.getAnonymousElementByAttribute(this, "class",
+ "menupopup-drop-indicator-bar");
+ </field>
+
+ <field name="_scrollBox">
+ document.getAnonymousElementByAttribute(this, "class",
+ "popup-internal-box");
+ </field>
+
+ <!-- This is the view that manage the popup -->
+ <field name="_rootView">PlacesUIUtils.getViewForNode(this);</field>
+
+ <!-- Check if we should hide the drop indicator for the target -->
+ <method name="_hideDropIndicator">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ let target = aEvent.target;
+
+ // Don't draw the drop indicator outside of markers or if current
+ // node is not a Places node.
+ let betweenMarkers =
+ (this._startMarker.compareDocumentPosition(target) & Node.DOCUMENT_POSITION_FOLLOWING) &&
+ (this._endMarker.compareDocumentPosition(target) & Node.DOCUMENT_POSITION_PRECEDING);
+
+ // Hide the dropmarker if current node is not a Places node.
+ return !(target && target._placesNode && betweenMarkers);
+ ]]></body>
+ </method>
+
+ <!-- This function returns information about where to drop when
+ dragging over this popup insertion point -->
+ <method name="_getDropPoint">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ // Can't drop if the menu isn't a folder
+ let resultNode = this._placesNode;
+
+ if (!PlacesUtils.nodeIsFolder(resultNode) ||
+ PlacesControllerDragHelper.disallowInsertion(resultNode)) {
+ return null;
+ }
+
+ var dropPoint = { ip: null, folderElt: null };
+
+ // The element we are dragging over
+ let elt = aEvent.target;
+ if (elt.localName == "menupopup")
+ elt = elt.parentNode;
+
+ // Calculate positions taking care of arrowscrollbox
+ let scrollbox = this._scrollBox;
+ let eventY = aEvent.layerY + (scrollbox.boxObject.y - this.boxObject.y);
+ let scrollboxOffset = scrollbox.scrollBoxObject.y -
+ (scrollbox.boxObject.y - this.boxObject.y);
+ let eltY = elt.boxObject.y - scrollboxOffset;
+ let eltHeight = elt.boxObject.height;
+
+ if (!elt._placesNode) {
+ // If we are dragging over a non places node drop at the end.
+ dropPoint.ip = new InsertionPoint(
+ PlacesUtils.getConcreteItemId(resultNode),
+ -1,
+ Ci.nsITreeView.DROP_ON);
+ // We can set folderElt if we are dropping over a static menu that
+ // has an internal placespopup.
+ let isMenu = elt.localName == "menu" ||
+ (elt.localName == "toolbarbutton" &&
+ elt.getAttribute("type") == "menu");
+ if (isMenu && elt.lastChild &&
+ elt.lastChild.hasAttribute("placespopup"))
+ dropPoint.folderElt = elt;
+ return dropPoint;
+ }
+
+ let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) ?
+ elt._placesNode.title : null;
+ if ((PlacesUtils.nodeIsFolder(elt._placesNode) &&
+ !PlacesUIUtils.isContentsReadOnly(elt._placesNode)) ||
+ PlacesUtils.nodeIsTagQuery(elt._placesNode)) {
+ // This is a folder or a tag container.
+ if (eventY - eltY < eltHeight * 0.20) {
+ // If mouse is in the top part of the element, drop above folder.
+ dropPoint.ip = new InsertionPoint(
+ PlacesUtils.getConcreteItemId(resultNode),
+ -1,
+ Ci.nsITreeView.DROP_BEFORE,
+ tagName,
+ elt._placesNode.itemId);
+ return dropPoint;
+ }
+ else if (eventY - eltY < eltHeight * 0.80) {
+ // If mouse is in the middle of the element, drop inside folder.
+ dropPoint.ip = new InsertionPoint(
+ PlacesUtils.getConcreteItemId(elt._placesNode),
+ -1,
+ Ci.nsITreeView.DROP_ON,
+ tagName);
+ dropPoint.folderElt = elt;
+ return dropPoint;
+ }
+ }
+ else if (eventY - eltY <= eltHeight / 2) {
+ // This is a non-folder node or a readonly folder.
+ // If the mouse is above the middle, drop above this item.
+ dropPoint.ip = new InsertionPoint(
+ PlacesUtils.getConcreteItemId(resultNode),
+ -1,
+ Ci.nsITreeView.DROP_BEFORE,
+ tagName,
+ elt._placesNode.itemId);
+ return dropPoint;
+ }
+
+ // Drop below the item.
+ dropPoint.ip = new InsertionPoint(
+ PlacesUtils.getConcreteItemId(resultNode),
+ -1,
+ Ci.nsITreeView.DROP_AFTER,
+ tagName,
+ elt._placesNode.itemId);
+ return dropPoint;
+ ]]></body>
+ </method>
+
+ <!-- Sub-menus should be opened when the mouse drags over them, and closed
+ when the mouse drags off. The overFolder object manages opening and
+ closing of folders when the mouse hovers. -->
+ <field name="_overFolder"><![CDATA[({
+ _self: this,
+ _folder: {elt: null,
+ openTimer: null,
+ hoverTime: 350,
+ closeTimer: null},
+ _closeMenuTimer: null,
+
+ get elt() {
+ return this._folder.elt;
+ },
+ set elt(val) {
+ return this._folder.elt = val;
+ },
+
+ get openTimer() {
+ return this._folder.openTimer;
+ },
+ set openTimer(val) {
+ return this._folder.openTimer = val;
+ },
+
+ get hoverTime() {
+ return this._folder.hoverTime;
+ },
+ set hoverTime(val) {
+ return this._folder.hoverTime = val;
+ },
+
+ get closeTimer() {
+ return this._folder.closeTimer;
+ },
+ set closeTimer(val) {
+ return this._folder.closeTimer = val;
+ },
+
+ get closeMenuTimer() {
+ return this._closeMenuTimer;
+ },
+ set closeMenuTimer(val) {
+ return this._closeMenuTimer = val;
+ },
+
+ setTimer: function OF__setTimer(aTime) {
+ var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT);
+ return timer;
+ },
+
+ notify: function OF__notify(aTimer) {
+ // Function to process all timer notifications.
+
+ if (aTimer == this._folder.openTimer) {
+ // Timer to open a submenu that's being dragged over.
+ this._folder.elt.lastChild.setAttribute("autoopened", "true");
+ this._folder.elt.lastChild.showPopup(this._folder.elt);
+ this._folder.openTimer = null;
+ }
+
+ else if (aTimer == this._folder.closeTimer) {
+ // Timer to close a submenu that's been dragged off of.
+ // Only close the submenu if the mouse isn't being dragged over any
+ // of its child menus.
+ var draggingOverChild = PlacesControllerDragHelper
+ .draggingOverChildNode(this._folder.elt);
+ if (draggingOverChild)
+ this._folder.elt = null;
+ this.clear();
+
+ // Close any parent folders which aren't being dragged over.
+ // (This is necessary because of the above code that keeps a folder
+ // open while its children are being dragged over.)
+ if (!draggingOverChild)
+ this.closeParentMenus();
+ }
+
+ else if (aTimer == this.closeMenuTimer) {
+ // Timer to close this menu after the drag exit.
+ var popup = this._self;
+ // if we are no more dragging we can leave the menu open to allow
+ // for better D&D bookmark organization
+ if (PlacesControllerDragHelper.getSession() &&
+ !PlacesControllerDragHelper.draggingOverChildNode(popup.parentNode)) {
+ popup.hidePopup();
+ // Close any parent menus that aren't being dragged over;
+ // otherwise they'll stay open because they couldn't close
+ // while this menu was being dragged over.
+ this.closeParentMenus();
+ }
+ this._closeMenuTimer = null;
+ }
+ },
+
+ // Helper function to close all parent menus of this menu,
+ // as long as none of the parent's children are currently being
+ // dragged over.
+ closeParentMenus: function OF__closeParentMenus() {
+ var popup = this._self;
+ var parent = popup.parentNode;
+ while (parent) {
+ if (parent.localName == "menupopup" && parent._placesNode) {
+ if (PlacesControllerDragHelper.draggingOverChildNode(parent.parentNode))
+ break;
+ parent.hidePopup();
+ }
+ parent = parent.parentNode;
+ }
+ },
+
+ // The mouse is no longer dragging over the stored menubutton.
+ // Close the menubutton, clear out drag styles, and clear all
+ // timers for opening/closing it.
+ clear: function OF__clear() {
+ if (this._folder.elt && this._folder.elt.lastChild) {
+ if (!this._folder.elt.lastChild.hasAttribute("dragover"))
+ this._folder.elt.lastChild.hidePopup();
+ // remove menuactive style
+ this._folder.elt.removeAttribute("_moz-menuactive");
+ this._folder.elt = null;
+ }
+ if (this._folder.openTimer) {
+ this._folder.openTimer.cancel();
+ this._folder.openTimer = null;
+ }
+ if (this._folder.closeTimer) {
+ this._folder.closeTimer.cancel();
+ this._folder.closeTimer = null;
+ }
+ }
+ })]]></field>
+
+ <method name="_cleanupDragDetails">
+ <body><![CDATA[
+ // Called on dragend and drop.
+ PlacesControllerDragHelper.currentDropTarget = null;
+ this._rootView._draggedElt = null;
+ this.removeAttribute("dragover");
+ this.removeAttribute("dragstart");
+ this._indicatorBar.hidden = true;
+ ]]></body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+ <handler event="DOMMenuItemActive"><![CDATA[
+ let elt = event.target;
+ if (elt.parentNode != this)
+ return;
+
+ if (this.AppConstants.platform === "macosx") {
+ // XXX: The following check is a temporary hack until bug 420033 is
+ // resolved.
+ let parentElt = elt.parent;
+ while (parentElt) {
+ if (parentElt.id == "bookmarksMenuPopup" ||
+ parentElt.id == "goPopup")
+ return;
+
+ parentElt = parentElt.parentNode;
+ }
+ }
+
+ if (window.XULBrowserWindow) {
+ let elt = event.target;
+ let placesNode = elt._placesNode;
+
+ var linkURI;
+ if (placesNode && PlacesUtils.nodeIsURI(placesNode))
+ linkURI = placesNode.uri;
+ else if (elt.hasAttribute("targetURI"))
+ linkURI = elt.getAttribute("targetURI");
+
+ if (linkURI)
+ window.XULBrowserWindow.setOverLink(linkURI, null);
+ }
+ ]]></handler>
+
+ <handler event="DOMMenuItemInactive"><![CDATA[
+ let elt = event.target;
+ if (elt.parentNode != this)
+ return;
+
+ if (window.XULBrowserWindow)
+ window.XULBrowserWindow.setOverLink("", null);
+ ]]></handler>
+
+ <handler event="dragstart"><![CDATA[
+ let elt = event.target;
+ if (!elt._placesNode)
+ return;
+
+ let draggedElt = elt._placesNode;
+
+ // Force a copy action if parent node is a query or we are dragging a
+ // not-removable node.
+ if (!PlacesControllerDragHelper.canMoveNode(draggedElt, elt))
+ event.dataTransfer.effectAllowed = "copyLink";
+
+ // Activate the view and cache the dragged element.
+ this._rootView._draggedElt = draggedElt;
+ this._rootView.controller.setDataTransfer(event);
+ this.setAttribute("dragstart", "true");
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="drop"><![CDATA[
+ PlacesControllerDragHelper.currentDropTarget = event.target;
+
+ let dropPoint = this._getDropPoint(event);
+ if (dropPoint && dropPoint.ip) {
+ PlacesControllerDragHelper.onDrop(dropPoint.ip, event.dataTransfer)
+ .then(null, Components.utils.reportError);
+ event.preventDefault();
+ }
+
+ this._cleanupDragDetails();
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragover"><![CDATA[
+ PlacesControllerDragHelper.currentDropTarget = event.target;
+ let dt = event.dataTransfer;
+
+ let dropPoint = this._getDropPoint(event);
+ if (!dropPoint || !dropPoint.ip ||
+ !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)) {
+ this._indicatorBar.hidden = true;
+ event.stopPropagation();
+ return;
+ }
+
+ // Mark this popup as being dragged over.
+ this.setAttribute("dragover", "true");
+
+ if (dropPoint.folderElt) {
+ // We are dragging over a folder.
+ // _overFolder should take the care of opening it on a timer.
+ if (this._overFolder.elt &&
+ this._overFolder.elt != dropPoint.folderElt) {
+ // We are dragging over a new folder, let's clear old values
+ this._overFolder.clear();
+ }
+ if (!this._overFolder.elt) {
+ this._overFolder.elt = dropPoint.folderElt;
+ // Create the timer to open this folder.
+ this._overFolder.openTimer = this._overFolder
+ .setTimer(this._overFolder.hoverTime);
+ }
+ // Since we are dropping into a folder set the corresponding style.
+ dropPoint.folderElt.setAttribute("_moz-menuactive", true);
+ }
+ else {
+ // We are not dragging over a folder.
+ // Clear out old _overFolder information.
+ this._overFolder.clear();
+ }
+
+ // Autoscroll the popup strip if we drag over the scroll buttons.
+ let anonid = event.originalTarget.getAttribute('anonid');
+ let scrollDir = 0;
+ if (anonid == "scrollbutton-up") {
+ scrollDir = -1;
+ } else if (anonid == "scrollbutton-down") {
+ scrollDir = 1;
+ }
+ if (scrollDir != 0) {
+ this._scrollBox.scrollByIndex(scrollDir, false);
+ }
+
+ // Check if we should hide the drop indicator for this target.
+ if (dropPoint.folderElt || this._hideDropIndicator(event)) {
+ this._indicatorBar.hidden = true;
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
+ // We should display the drop indicator relative to the arrowscrollbox.
+ let sbo = this._scrollBox.scrollBoxObject;
+ let newMarginTop = 0;
+ if (scrollDir == 0) {
+ let elt = this.firstChild;
+ while (elt && event.screenY > elt.boxObject.screenY +
+ elt.boxObject.height / 2)
+ elt = elt.nextSibling;
+ newMarginTop = elt ? elt.boxObject.screenY - sbo.screenY :
+ sbo.height;
+ }
+ else if (scrollDir == 1)
+ newMarginTop = sbo.height;
+
+ // Set the new marginTop based on arrowscrollbox.
+ newMarginTop += sbo.y - this._scrollBox.boxObject.y;
+ this._indicatorBar.firstChild.style.marginTop = newMarginTop + "px";
+ this._indicatorBar.hidden = false;
+
+ event.preventDefault();
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragexit"><![CDATA[
+ PlacesControllerDragHelper.currentDropTarget = null;
+ this.removeAttribute("dragover");
+
+ // If we have not moved to a valid new target clear the drop indicator
+ // this happens when moving out of the popup.
+ let target = event.relatedTarget;
+ if (!target || !this.contains(target))
+ this._indicatorBar.hidden = true;
+
+ // Close any folder being hovered over
+ if (this._overFolder.elt) {
+ this._overFolder.closeTimer = this._overFolder
+ .setTimer(this._overFolder.hoverTime);
+ }
+
+ // The autoopened attribute is set when this folder was automatically
+ // opened after the user dragged over it. If this attribute is set,
+ // auto-close the folder on drag exit.
+ // We should also try to close this popup if the drag has started
+ // from here, the timer will check if we are dragging over a child.
+ if (this.hasAttribute("autoopened") ||
+ this.hasAttribute("dragstart")) {
+ this._overFolder.closeMenuTimer = this._overFolder
+ .setTimer(this._overFolder.hoverTime);
+ }
+
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragend"><![CDATA[
+ this._cleanupDragDetails();
+ ]]></handler>
+
+ </handlers>
+ </binding>
+
+ <!-- Most of this is copied from the arrowpanel binding in popup.xml -->
+ <binding id="places-popup-arrow"
+ extends="chrome://browser/content/places/menu.xml#places-popup-base">
+ <content flip="both" side="top" position="bottomcenter topright">
+ <xul:vbox anonid="container" class="panel-arrowcontainer" flex="1"
+ xbl:inherits="side,panelopen">
+ <xul:box anonid="arrowbox" class="panel-arrowbox">
+ <xul:image anonid="arrow" class="panel-arrow" xbl:inherits="side"/>
+ </xul:box>
+ <xul:box class="panel-arrowcontent" xbl:inherits="side,align,dir,orient,pack" flex="1">
+ <xul:vbox class="menupopup-drop-indicator-bar" hidden="true">
+ <xul:image class="menupopup-drop-indicator" mousethrough="always"/>
+ </xul:vbox>
+ <xul:arrowscrollbox class="popup-internal-box" flex="1" orient="vertical"
+ smoothscroll="false">
+ <children/>
+ </xul:arrowscrollbox>
+ </xul:box>
+ </xul:vbox>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ this.style.pointerEvents = 'none';
+ ]]></constructor>
+ <method name="adjustArrowPosition">
+ <body><![CDATA[
+ var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow");
+
+ var anchor = this.anchorNode;
+ if (!anchor) {
+ arrow.hidden = true;
+ return;
+ }
+
+ var container = document.getAnonymousElementByAttribute(this, "anonid", "container");
+ var arrowbox = document.getAnonymousElementByAttribute(this, "anonid", "arrowbox");
+
+ var position = this.alignmentPosition;
+ var offset = this.alignmentOffset;
+
+ this.setAttribute("arrowposition", position);
+
+ // if this panel has a "sliding" arrow, we may have previously set margins...
+ arrowbox.style.removeProperty("transform");
+ if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) {
+ container.orient = "horizontal";
+ arrowbox.orient = "vertical";
+ if (position.indexOf("_after") > 0) {
+ arrowbox.pack = "end";
+ } else {
+ arrowbox.pack = "start";
+ }
+ arrowbox.style.transform = "translate(0, " + -offset + "px)";
+
+ // The assigned side stays the same regardless of direction.
+ var isRTL = (window.getComputedStyle(this).direction == "rtl");
+
+ if (position.indexOf("start_") == 0) {
+ container.dir = "reverse";
+ this.setAttribute("side", isRTL ? "left" : "right");
+ }
+ else {
+ container.dir = "";
+ this.setAttribute("side", isRTL ? "right" : "left");
+ }
+ }
+ else if (position.indexOf("before_") == 0 || position.indexOf("after_") == 0) {
+ container.orient = "";
+ arrowbox.orient = "";
+ if (position.indexOf("_end") > 0) {
+ arrowbox.pack = "end";
+ } else {
+ arrowbox.pack = "start";
+ }
+ arrowbox.style.transform = "translate(" + -offset + "px, 0)";
+
+ if (position.indexOf("before_") == 0) {
+ container.dir = "reverse";
+ this.setAttribute("side", "bottom");
+ }
+ else {
+ container.dir = "";
+ this.setAttribute("side", "top");
+ }
+ }
+
+ arrow.hidden = false;
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="popupshowing" phase="target"><![CDATA[
+ this.adjustArrowPosition();
+ this.setAttribute("animate", "open");
+ ]]></handler>
+ <handler event="popupshown" phase="target"><![CDATA[
+ this.setAttribute("panelopen", "true");
+ let disablePointerEvents;
+ if (!this.hasAttribute("disablepointereventsfortransition")) {
+ let container = document.getAnonymousElementByAttribute(this, "anonid", "container");
+ let cs = getComputedStyle(container);
+ let transitionProp = cs.transitionProperty;
+ let transitionTime = parseFloat(cs.transitionDuration);
+ disablePointerEvents = (transitionProp.includes("transform") ||
+ transitionProp == "all") &&
+ transitionTime > 0;
+ this.setAttribute("disablepointereventsfortransition", disablePointerEvents);
+ } else {
+ disablePointerEvents = this.getAttribute("disablepointereventsfortransition") == "true";
+ }
+ if (!disablePointerEvents) {
+ this.style.removeProperty("pointer-events");
+ }
+ ]]></handler>
+ <handler event="transitionend"><![CDATA[
+ if (event.originalTarget.getAttribute("anonid") == "container" &&
+ event.propertyName == "transform") {
+ this.style.removeProperty("pointer-events");
+ }
+ ]]></handler>
+ <handler event="popuphiding" phase="target"><![CDATA[
+ this.setAttribute("animate", "cancel");
+ ]]></handler>
+ <handler event="popuphidden" phase="target"><![CDATA[
+ this.removeAttribute("panelopen");
+ if (this.getAttribute("disablepointereventsfortransition") == "true") {
+ this.style.pointerEvents = 'none';
+ }
+ this.removeAttribute("animate");
+ ]]></handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/browser/components/places/content/moveBookmarks.js b/browser/components/places/content/moveBookmarks.js
new file mode 100644
index 000000000..5bfdce56e
--- /dev/null
+++ b/browser/components/places/content/moveBookmarks.js
@@ -0,0 +1,65 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var gMoveBookmarksDialog = {
+ _nodes: null,
+
+ _foldersTree: null,
+ get foldersTree() {
+ if (!this._foldersTree)
+ this._foldersTree = document.getElementById("foldersTree");
+
+ return this._foldersTree;
+ },
+
+ init: function() {
+ this._nodes = window.arguments[0];
+
+ this.foldersTree.place =
+ "place:excludeItems=1&excludeQueries=1&excludeReadOnlyFolders=1&folder=" +
+ PlacesUIUtils.allBookmarksFolderId;
+ },
+
+ onOK: function MBD_onOK(aEvent) {
+ let selectedNode = this.foldersTree.selectedNode;
+ let selectedFolderId = PlacesUtils.getConcreteItemId(selectedNode);
+
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let transactions = [];
+ for (var i=0; i < this._nodes.length; i++) {
+ // Nothing to do if the node is already under the selected folder
+ if (this._nodes[i].parent.itemId == selectedFolderId)
+ continue;
+
+ let txn = new PlacesMoveItemTransaction(this._nodes[i].itemId,
+ selectedFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ transactions.push(txn);
+ }
+ if (transactions.length != 0) {
+ let txn = new PlacesAggregatedTransaction("Move Items", transactions);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ }
+ return;
+ }
+
+ PlacesTransactions.batch(function* () {
+ let newParentGuid = yield PlacesUtils.promiseItemGuid(selectedFolderId);
+ for (let node of this._nodes) {
+ // Nothing to do if the node is already under the selected folder.
+ if (node.parent.itemId == selectedFolderId)
+ continue;
+ yield PlacesTransactions.Move({ guid: node.bookmarkGuid
+ , newParentGuid }).transact();
+ }
+ }.bind(this)).then(null, Components.utils.reportError);
+ },
+
+ newFolder: function MBD_newFolder() {
+ // The command is disabled when the tree is not focused
+ this.foldersTree.focus();
+ goDoCommand("placesCmd_new:folder");
+ }
+};
diff --git a/browser/components/places/content/moveBookmarks.xul b/browser/components/places/content/moveBookmarks.xul
new file mode 100644
index 000000000..b6e75f3da
--- /dev/null
+++ b/browser/components/places/content/moveBookmarks.xul
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<!DOCTYPE window [
+ <!ENTITY % moveBookmarksDTD SYSTEM "chrome://browser/locale/places/moveBookmarks.dtd">
+ %moveBookmarksDTD;
+]>
+
+<dialog id="moveBookmarkDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ ondialogaccept="return gMoveBookmarksDialog.onOK(event);"
+ title="&window.title;"
+ onload="gMoveBookmarksDialog.init();"
+ style="&window.style;"
+ screenX="24"
+ screenY="24"
+ persist="screenX screenY width height">
+
+ <script type="application/javascript"
+ src="chrome://browser/content/places/moveBookmarks.js"/>
+
+ <hbox flex="1">
+ <label id="movetolabel" value="&moveTo.label;" control="foldersTree"/>
+ <hbox flex="1">
+ <tree id="foldersTree"
+ class="placesTree"
+ flex="1"
+ type="places"
+ seltype="single"
+ hidecolumnpicker="true">
+ <treecols>
+ <treecol id="title" flex="1" primary="true" hideheader="true"/>
+ </treecols>
+ <treechildren id="placesListChildren" view="placesList" flex="1"/>
+ </tree>
+ <vbox>
+ <button id="newFolderButton"
+ label="&newFolderButton.label;"
+ accesskey="&newFolderButton.accesskey;"
+ oncommand="gMoveBookmarksDialog.newFolder();"/>
+ </vbox>
+ </hbox>
+ </hbox>
+</dialog>
diff --git a/browser/components/places/content/organizer.css b/browser/components/places/content/organizer.css
new file mode 100644
index 000000000..47b1832c1
--- /dev/null
+++ b/browser/components/places/content/organizer.css
@@ -0,0 +1,7 @@
+/* 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/. */
+
+#searchFilter {
+ width: 23em;
+}
diff --git a/browser/components/places/content/places.css b/browser/components/places/content/places.css
new file mode 100644
index 000000000..de3cc91d8
--- /dev/null
+++ b/browser/components/places/content/places.css
@@ -0,0 +1,25 @@
+/* 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/. */
+
+tree[type="places"] {
+ -moz-binding: url("chrome://browser/content/places/tree.xml#places-tree");
+}
+
+.toolbar-drop-indicator {
+ position: relative;
+ z-index: 1;
+}
+
+menupopup[placespopup="true"] {
+ -moz-binding: url("chrome://browser/content/places/menu.xml#places-popup-base");
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+ #bookmarksChildren,
+ .sidebar-placesTreechildren,
+ .placesTree > treechildren {
+ image-rendering: -moz-crisp-edges;
+ }
+}
diff --git a/browser/components/places/content/places.js b/browser/components/places/content/places.js
new file mode 100644
index 000000000..aa43b20e6
--- /dev/null
+++ b/browser/components/places/content/places.js
@@ -0,0 +1,1405 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/TelemetryStopwatch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MigrationUtils",
+ "resource:///modules/MigrationUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
+ "resource://gre/modules/BookmarkJSONUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
+ "resource://gre/modules/PlacesBackups.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
+ "resource://gre/modules/DownloadUtils.jsm");
+
+const RESTORE_FILEPICKER_FILTER_EXT = "*.json;*.jsonlz4";
+const HISTORY_LIBRARY_SEARCH_TELEMETRY = "PLACES_HISTORY_LIBRARY_SEARCH_TIME_MS";
+
+var PlacesOrganizer = {
+ _places: null,
+
+ // IDs of fields from editBookmarkOverlay that should be hidden when infoBox
+ // is minimal. IDs should be kept in sync with the IDs of the elements
+ // observing additionalInfoBroadcaster.
+ _additionalInfoFields: [
+ "editBMPanel_descriptionRow",
+ "editBMPanel_loadInSidebarCheckbox",
+ "editBMPanel_keywordRow",
+ ],
+
+ _initFolderTree: function() {
+ var leftPaneRoot = PlacesUIUtils.leftPaneFolderId;
+ this._places.place = "place:excludeItems=1&expandQueries=0&folder=" + leftPaneRoot;
+ },
+
+ selectLeftPaneQuery: function PO_selectLeftPaneQuery(aQueryName) {
+ var itemId = PlacesUIUtils.leftPaneQueries[aQueryName];
+ this._places.selectItems([itemId]);
+ // Forcefully expand all-bookmarks
+ if (aQueryName == "AllBookmarks" || aQueryName == "History")
+ PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
+ },
+
+ /**
+ * Opens a given hierarchy in the left pane, stopping at the last reachable
+ * container.
+ *
+ * @param aHierarchy A single container or an array of containers, sorted from
+ * the outmost to the innermost in the hierarchy. Each
+ * container may be either an item id, a Places URI string,
+ * or a named query.
+ * @see PlacesUIUtils.leftPaneQueries for supported named queries.
+ */
+ selectLeftPaneContainerByHierarchy:
+ function PO_selectLeftPaneContainerByHierarchy(aHierarchy) {
+ if (!aHierarchy)
+ throw new Error("Invalid containers hierarchy");
+ let hierarchy = [].concat(aHierarchy);
+ let selectWasSuppressed = this._places.view.selection.selectEventsSuppressed;
+ if (!selectWasSuppressed)
+ this._places.view.selection.selectEventsSuppressed = true;
+ try {
+ for (let container of hierarchy) {
+ switch (typeof container) {
+ case "number":
+ this._places.selectItems([container], false);
+ break;
+ case "string":
+ if (container.substr(0, 6) == "place:")
+ this._places.selectPlaceURI(container);
+ else if (container in PlacesUIUtils.leftPaneQueries)
+ this.selectLeftPaneQuery(container);
+ else
+ throw new Error("Invalid container found: " + container);
+ break;
+ default:
+ throw new Error("Invalid container type found: " + container);
+ }
+ PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
+ }
+ } finally {
+ if (!selectWasSuppressed)
+ this._places.view.selection.selectEventsSuppressed = false;
+ }
+ },
+
+ init: function PO_init() {
+ ContentArea.init();
+
+ this._places = document.getElementById("placesList");
+ this._initFolderTree();
+
+ var leftPaneSelection = "AllBookmarks"; // default to all-bookmarks
+ if (window.arguments && window.arguments[0])
+ leftPaneSelection = window.arguments[0];
+
+ this.selectLeftPaneContainerByHierarchy(leftPaneSelection);
+ if (leftPaneSelection === "History") {
+ let historyNode = this._places.selectedNode;
+ if (historyNode.childCount > 0)
+ this._places.selectNode(historyNode.getChild(0));
+ }
+
+ // clear the back-stack
+ this._backHistory.splice(0, this._backHistory.length);
+ document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
+
+ // Set up the search UI.
+ PlacesSearchBox.init();
+
+ window.addEventListener("AppCommand", this, true);
+
+ if (AppConstants.platform === "macosx") {
+ // 1. Map Edit->Find command to OrganizerCommand_find:all. Need to map
+ // both the menuitem and the Find key.
+ let findMenuItem = document.getElementById("menu_find");
+ findMenuItem.setAttribute("command", "OrganizerCommand_find:all");
+ let findKey = document.getElementById("key_find");
+ findKey.setAttribute("command", "OrganizerCommand_find:all");
+
+ // 2. Disable some keybindings from browser.xul
+ let elements = ["cmd_handleBackspace", "cmd_handleShiftBackspace"];
+ for (let i = 0; i < elements.length; i++) {
+ document.getElementById(elements[i]).setAttribute("disabled", "true");
+ }
+ }
+
+ // remove the "Properties" context-menu item, we've our own details pane
+ document.getElementById("placesContext")
+ .removeChild(document.getElementById("placesContext_show:info"));
+
+ ContentArea.focus();
+ },
+
+ QueryInterface: function PO_QueryInterface(aIID) {
+ if (aIID.equals(Components.interfaces.nsIDOMEventListener) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+
+ throw Components.results.NS_NOINTERFACE;
+ },
+
+ handleEvent: function PO_handleEvent(aEvent) {
+ if (aEvent.type != "AppCommand")
+ return;
+
+ aEvent.stopPropagation();
+ switch (aEvent.command) {
+ case "Back":
+ if (this._backHistory.length > 0)
+ this.back();
+ break;
+ case "Forward":
+ if (this._forwardHistory.length > 0)
+ this.forward();
+ break;
+ case "Search":
+ PlacesSearchBox.findAll();
+ break;
+ }
+ },
+
+ destroy: function PO_destroy() {
+ },
+
+ _location: null,
+ get location() {
+ return this._location;
+ },
+
+ set location(aLocation) {
+ if (!aLocation || this._location == aLocation)
+ return aLocation;
+
+ if (this.location) {
+ this._backHistory.unshift(this.location);
+ this._forwardHistory.splice(0, this._forwardHistory.length);
+ }
+
+ this._location = aLocation;
+ this._places.selectPlaceURI(aLocation);
+
+ if (!this._places.hasSelection) {
+ // If no node was found for the given place: uri, just load it directly
+ ContentArea.currentPlace = aLocation;
+ }
+ this.updateDetailsPane();
+
+ // update navigation commands
+ if (this._backHistory.length == 0)
+ document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
+ else
+ document.getElementById("OrganizerCommand:Back").removeAttribute("disabled");
+ if (this._forwardHistory.length == 0)
+ document.getElementById("OrganizerCommand:Forward").setAttribute("disabled", true);
+ else
+ document.getElementById("OrganizerCommand:Forward").removeAttribute("disabled");
+
+ return aLocation;
+ },
+
+ _backHistory: [],
+ _forwardHistory: [],
+
+ back: function PO_back() {
+ this._forwardHistory.unshift(this.location);
+ var historyEntry = this._backHistory.shift();
+ this._location = null;
+ this.location = historyEntry;
+ },
+ forward: function PO_forward() {
+ this._backHistory.unshift(this.location);
+ var historyEntry = this._forwardHistory.shift();
+ this._location = null;
+ this.location = historyEntry;
+ },
+
+ /**
+ * Called when a place folder is selected in the left pane.
+ * @param resetSearchBox
+ * true if the search box should also be reset, false otherwise.
+ * The search box should be reset when a new folder in the left
+ * pane is selected; the search scope and text need to be cleared in
+ * preparation for the new folder. Note that if the user manually
+ * resets the search box, either by clicking its reset button or by
+ * deleting its text, this will be false.
+ */
+ _cachedLeftPaneSelectedURI: null,
+ onPlaceSelected: function PO_onPlaceSelected(resetSearchBox) {
+ // Don't change the right-hand pane contents when there's no selection.
+ if (!this._places.hasSelection)
+ return;
+
+ var node = this._places.selectedNode;
+ var queries = PlacesUtils.asQuery(node).getQueries();
+
+ // Items are only excluded on the left pane.
+ var options = node.queryOptions.clone();
+ options.excludeItems = false;
+ var placeURI = PlacesUtils.history.queriesToQueryString(queries,
+ queries.length,
+ options);
+
+ // If either the place of the content tree in the right pane has changed or
+ // the user cleared the search box, update the place, hide the search UI,
+ // and update the back/forward buttons by setting location.
+ if (ContentArea.currentPlace != placeURI || !resetSearchBox) {
+ ContentArea.currentPlace = placeURI;
+ this.location = node.uri;
+ }
+
+ // When we invalidate a container we use suppressSelectionEvent, when it is
+ // unset a select event is fired, in many cases the selection did not really
+ // change, so we should check for it, and return early in such a case. Note
+ // that we cannot return any earlier than this point, because when
+ // !resetSearchBox, we need to update location and hide the UI as above,
+ // even though the selection has not changed.
+ if (node.uri == this._cachedLeftPaneSelectedURI)
+ return;
+ this._cachedLeftPaneSelectedURI = node.uri;
+
+ // At this point, resetSearchBox is true, because the left pane selection
+ // has changed; otherwise we would have returned earlier.
+
+ PlacesSearchBox.searchFilter.reset();
+ this._setSearchScopeForNode(node);
+ this.updateDetailsPane();
+ },
+
+ /**
+ * Sets the search scope based on aNode's properties.
+ * @param aNode
+ * the node to set up scope from
+ */
+ _setSearchScopeForNode: function PO__setScopeForNode(aNode) {
+ let itemId = aNode.itemId;
+
+ if (PlacesUtils.nodeIsHistoryContainer(aNode) ||
+ itemId == PlacesUIUtils.leftPaneQueries["History"]) {
+ PlacesQueryBuilder.setScope("history");
+ }
+ else if (itemId == PlacesUIUtils.leftPaneQueries["Downloads"]) {
+ PlacesQueryBuilder.setScope("downloads");
+ }
+ else {
+ // Default to All Bookmarks for all other nodes, per bug 469437.
+ PlacesQueryBuilder.setScope("bookmarks");
+ }
+ },
+
+ /**
+ * Handle clicks on the places list.
+ * Single Left click, right click or modified click do not result in any
+ * special action, since they're related to selection.
+ * @param aEvent
+ * The mouse event.
+ */
+ onPlacesListClick: function PO_onPlacesListClick(aEvent) {
+ // Only handle clicks on tree children.
+ if (aEvent.target.localName != "treechildren")
+ return;
+
+ let node = this._places.selectedNode;
+ if (node) {
+ let middleClick = aEvent.button == 1 && aEvent.detail == 1;
+ if (middleClick && PlacesUtils.nodeIsContainer(node)) {
+ // The command execution function will take care of seeing if the
+ // selection is a folder or a different container type, and will
+ // load its contents in tabs.
+ PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent, this._places);
+ }
+ }
+ },
+
+ /**
+ * Handle focus changes on the places list and the current content view.
+ */
+ updateDetailsPane: function PO_updateDetailsPane() {
+ if (!ContentArea.currentViewOptions.showDetailsPane)
+ return;
+ let view = PlacesUIUtils.getViewForNode(document.activeElement);
+ if (view) {
+ let selectedNodes = view.selectedNode ?
+ [view.selectedNode] : view.selectedNodes;
+ this._fillDetailsPane(selectedNodes);
+ }
+ },
+
+ openFlatContainer: function PO_openFlatContainerFlatContainer(aContainer) {
+ if (aContainer.itemId != -1) {
+ PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
+ this._places.selectItems([aContainer.itemId], false);
+ }
+ else if (PlacesUtils.nodeIsQuery(aContainer)) {
+ this._places.selectPlaceURI(aContainer.uri);
+ }
+ },
+
+ /**
+ * Returns the options associated with the query currently loaded in the
+ * main places pane.
+ */
+ getCurrentOptions: function PO_getCurrentOptions() {
+ return PlacesUtils.asQuery(ContentArea.currentView.result.root).queryOptions;
+ },
+
+ /**
+ * Returns the queries associated with the query currently loaded in the
+ * main places pane.
+ */
+ getCurrentQueries: function PO_getCurrentQueries() {
+ return PlacesUtils.asQuery(ContentArea.currentView.result.root).getQueries();
+ },
+
+ /**
+ * Show the migration wizard for importing passwords,
+ * cookies, history, preferences, and bookmarks.
+ */
+ importFromBrowser: function PO_importFromBrowser() {
+ // We pass in the type of source we're using for use in telemetry:
+ MigrationUtils.showMigrationWizard(window, [MigrationUtils.MIGRATION_ENTRYPOINT_PLACES]);
+ },
+
+ /**
+ * Open a file-picker and import the selected file into the bookmarks store
+ */
+ importFromFile: function PO_importFromFile() {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult != Ci.nsIFilePicker.returnCancel && fp.fileURL) {
+ Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
+ BookmarkHTMLUtils.importFromURL(fp.fileURL.spec, false)
+ .then(null, Components.utils.reportError);
+ }
+ };
+
+ fp.init(window, PlacesUIUtils.getString("SelectImport"),
+ Ci.nsIFilePicker.modeOpen);
+ fp.appendFilters(Ci.nsIFilePicker.filterHTML);
+ fp.open(fpCallback);
+ },
+
+ /**
+ * Allows simple exporting of bookmarks.
+ */
+ exportBookmarks: function PO_exportBookmarks() {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult != Ci.nsIFilePicker.returnCancel) {
+ Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
+ BookmarkHTMLUtils.exportToFile(fp.file.path)
+ .then(null, Components.utils.reportError);
+ }
+ };
+
+ fp.init(window, PlacesUIUtils.getString("EnterExport"),
+ Ci.nsIFilePicker.modeSave);
+ fp.appendFilters(Ci.nsIFilePicker.filterHTML);
+ fp.defaultString = "bookmarks.html";
+ fp.open(fpCallback);
+ },
+
+ /**
+ * Populates the restore menu with the dates of the backups available.
+ */
+ populateRestoreMenu: function PO_populateRestoreMenu() {
+ let restorePopup = document.getElementById("fileRestorePopup");
+
+ const locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIXULChromeRegistry)
+ .getSelectedLocale("global", true);
+ const dtOptions = { year: 'numeric', month: 'long', day: 'numeric' };
+ let dateFormatter = new Intl.DateTimeFormat(locale, dtOptions);
+
+ // Remove existing menu items. Last item is the restoreFromFile item.
+ while (restorePopup.childNodes.length > 1)
+ restorePopup.removeChild(restorePopup.firstChild);
+
+ Task.spawn(function* () {
+ let backupFiles = yield PlacesBackups.getBackupFiles();
+ if (backupFiles.length == 0)
+ return;
+
+ // Populate menu with backups.
+ for (let i = 0; i < backupFiles.length; i++) {
+ let fileSize = (yield OS.File.stat(backupFiles[i])).size;
+ let [size, unit] = DownloadUtils.convertByteUnits(fileSize);
+ let sizeString = PlacesUtils.getFormattedString("backupFileSizeText",
+ [size, unit]);
+ let sizeInfo;
+ let bookmarkCount = PlacesBackups.getBookmarkCountForFile(backupFiles[i]);
+ if (bookmarkCount != null) {
+ sizeInfo = " (" + sizeString + " - " +
+ PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
+ bookmarkCount,
+ [bookmarkCount]) +
+ ")";
+ } else {
+ sizeInfo = " (" + sizeString + ")";
+ }
+
+ let backupDate = PlacesBackups.getDateForFile(backupFiles[i]);
+ let m = restorePopup.insertBefore(document.createElement("menuitem"),
+ document.getElementById("restoreFromFile"));
+ m.setAttribute("label", dateFormatter.format(backupDate) + sizeInfo);
+ m.setAttribute("value", OS.Path.basename(backupFiles[i]));
+ m.setAttribute("oncommand",
+ "PlacesOrganizer.onRestoreMenuItemClick(this);");
+ }
+
+ // Add the restoreFromFile item.
+ restorePopup.insertBefore(document.createElement("menuseparator"),
+ document.getElementById("restoreFromFile"));
+ });
+ },
+
+ /**
+ * Called when a menuitem is selected from the restore menu.
+ */
+ onRestoreMenuItemClick: Task.async(function* (aMenuItem) {
+ let backupName = aMenuItem.getAttribute("value");
+ let backupFilePaths = yield PlacesBackups.getBackupFiles();
+ for (let backupFilePath of backupFilePaths) {
+ if (OS.Path.basename(backupFilePath) == backupName) {
+ PlacesOrganizer.restoreBookmarksFromFile(backupFilePath);
+ break;
+ }
+ }
+ }),
+
+ /**
+ * Called when 'Choose File...' is selected from the restore menu.
+ * Prompts for a file and restores bookmarks to those in the file.
+ */
+ onRestoreBookmarksFromFile: function PO_onRestoreBookmarksFromFile() {
+ let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+ let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult != Ci.nsIFilePicker.returnCancel) {
+ this.restoreBookmarksFromFile(fp.file.path);
+ }
+ }.bind(this);
+
+ fp.init(window, PlacesUIUtils.getString("bookmarksRestoreTitle"),
+ Ci.nsIFilePicker.modeOpen);
+ fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
+ RESTORE_FILEPICKER_FILTER_EXT);
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.displayDirectory = backupsDir;
+ fp.open(fpCallback);
+ },
+
+ /**
+ * Restores bookmarks from a JSON file.
+ */
+ restoreBookmarksFromFile: function PO_restoreBookmarksFromFile(aFilePath) {
+ // check file extension
+ if (!aFilePath.toLowerCase().endsWith("json") &&
+ !aFilePath.toLowerCase().endsWith("jsonlz4")) {
+ this._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreFormatError"));
+ return;
+ }
+
+ // confirm ok to delete existing bookmarks
+ var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService);
+ if (!prompts.confirm(null,
+ PlacesUIUtils.getString("bookmarksRestoreAlertTitle"),
+ PlacesUIUtils.getString("bookmarksRestoreAlert")))
+ return;
+
+ Task.spawn(function* () {
+ try {
+ yield BookmarkJSONUtils.importFromFile(aFilePath, true);
+ } catch (ex) {
+ PlacesOrganizer._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreParseError"));
+ }
+ });
+ },
+
+ _showErrorAlert: function PO__showErrorAlert(aMsg) {
+ var brandShortName = document.getElementById("brandStrings").
+ getString("brandShortName");
+
+ Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService).
+ alert(window, brandShortName, aMsg);
+ },
+
+ /**
+ * Backup bookmarks to desktop, auto-generate a filename with a date.
+ * The file is a JSON serialization of bookmarks, tags and any annotations
+ * of those items.
+ */
+ backupBookmarks: function PO_backupBookmarks() {
+ let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+ let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult != Ci.nsIFilePicker.returnCancel) {
+ // There is no OS.File version of the filepicker yet (Bug 937812).
+ PlacesBackups.saveBookmarksToJSONFile(fp.file.path);
+ }
+ };
+
+ fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"),
+ Ci.nsIFilePicker.modeSave);
+ fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
+ RESTORE_FILEPICKER_FILTER_EXT);
+ fp.defaultString = PlacesBackups.getFilenameForDate();
+ fp.defaultExtension = "json";
+ fp.displayDirectory = backupsDir;
+ fp.open(fpCallback);
+ },
+
+ _detectAndSetDetailsPaneMinimalState:
+ function PO__detectAndSetDetailsPaneMinimalState(aNode) {
+ /**
+ * The details of simple folder-items (as opposed to livemarks) or the
+ * of livemark-children are not likely to fill the infoBox anyway,
+ * thus we remove the "More/Less" button and show all details.
+ *
+ * the wasminimal attribute here is used to persist the "more/less"
+ * state in a bookmark->folder->bookmark scenario.
+ */
+ var infoBox = document.getElementById("infoBox");
+ var infoBoxExpanderWrapper = document.getElementById("infoBoxExpanderWrapper");
+ var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster");
+
+ if (!aNode) {
+ infoBoxExpanderWrapper.hidden = true;
+ return;
+ }
+ if (aNode.itemId != -1 &&
+ PlacesUtils.nodeIsFolder(aNode) && !aNode._feedURI) {
+ if (infoBox.getAttribute("minimal") == "true")
+ infoBox.setAttribute("wasminimal", "true");
+ infoBox.removeAttribute("minimal");
+ infoBoxExpanderWrapper.hidden = true;
+ }
+ else {
+ if (infoBox.getAttribute("wasminimal") == "true")
+ infoBox.setAttribute("minimal", "true");
+ infoBox.removeAttribute("wasminimal");
+ infoBoxExpanderWrapper.hidden =
+ this._additionalInfoFields.every(id =>
+ document.getElementById(id).collapsed);
+ }
+ additionalInfoBroadcaster.hidden = infoBox.getAttribute("minimal") == "true";
+ },
+
+ // NOT YET USED
+ updateThumbnailProportions: function PO_updateThumbnailProportions() {
+ var previewBox = document.getElementById("previewBox");
+ var canvas = document.getElementById("itemThumbnail");
+ var height = previewBox.boxObject.height;
+ var width = height * (screen.width / screen.height);
+ canvas.width = width;
+ canvas.height = height;
+ },
+
+ _fillDetailsPane: function PO__fillDetailsPane(aNodeList) {
+ var infoBox = document.getElementById("infoBox");
+ var detailsDeck = document.getElementById("detailsDeck");
+
+ // Make sure the infoBox UI is visible if we need to use it, we hide it
+ // below when we don't.
+ infoBox.hidden = false;
+ let selectedNode = aNodeList.length == 1 ? aNodeList[0] : null;
+
+ // If a textbox within a panel is focused, force-blur it so its contents
+ // are saved
+ if (gEditItemOverlay.itemId != -1) {
+ var focusedElement = document.commandDispatcher.focusedElement;
+ if ((focusedElement instanceof HTMLInputElement ||
+ focusedElement instanceof HTMLTextAreaElement) &&
+ /^editBMPanel.*/.test(focusedElement.parentNode.parentNode.id))
+ focusedElement.blur();
+
+ // don't update the panel if we are already editing this node unless we're
+ // in multi-edit mode
+ if (selectedNode) {
+ let concreteId = PlacesUtils.getConcreteItemId(selectedNode);
+ var nodeIsSame = gEditItemOverlay.itemId == selectedNode.itemId ||
+ gEditItemOverlay.itemId == concreteId ||
+ (selectedNode.itemId == -1 && gEditItemOverlay.uri &&
+ gEditItemOverlay.uri == selectedNode.uri);
+ if (nodeIsSame && detailsDeck.selectedIndex == 1 &&
+ !gEditItemOverlay.multiEdit)
+ return;
+ }
+ }
+
+ // Clean up the panel before initing it again.
+ gEditItemOverlay.uninitPanel(false);
+
+ if (selectedNode && !PlacesUtils.nodeIsSeparator(selectedNode)) {
+ detailsDeck.selectedIndex = 1;
+
+ gEditItemOverlay.initPanel({ node: selectedNode
+ , hiddenRows: ["folderPicker"] });
+
+ this._detectAndSetDetailsPaneMinimalState(selectedNode);
+ }
+ else if (!selectedNode && aNodeList[0]) {
+ if (aNodeList.every(PlacesUtils.nodeIsURI)) {
+ let uris = aNodeList.map(node => PlacesUtils._uri(node.uri));
+ detailsDeck.selectedIndex = 1;
+ gEditItemOverlay.initPanel({ uris
+ , hiddenRows: ["folderPicker",
+ "loadInSidebar",
+ "location",
+ "keyword",
+ "description",
+ "name"]});
+ this._detectAndSetDetailsPaneMinimalState(selectedNode);
+ }
+ else {
+ detailsDeck.selectedIndex = 0;
+ let selectItemDesc = document.getElementById("selectItemDescription");
+ let itemsCountLabel = document.getElementById("itemsCountText");
+ selectItemDesc.hidden = false;
+ itemsCountLabel.value =
+ PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
+ aNodeList.length, [aNodeList.length]);
+ infoBox.hidden = true;
+ }
+ }
+ else {
+ detailsDeck.selectedIndex = 0;
+ infoBox.hidden = true;
+ let selectItemDesc = document.getElementById("selectItemDescription");
+ let itemsCountLabel = document.getElementById("itemsCountText");
+ let itemsCount = 0;
+ if (ContentArea.currentView.result) {
+ let rootNode = ContentArea.currentView.result.root;
+ if (rootNode.containerOpen)
+ itemsCount = rootNode.childCount;
+ }
+ if (itemsCount == 0) {
+ selectItemDesc.hidden = true;
+ itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.noItems");
+ }
+ else {
+ selectItemDesc.hidden = false;
+ itemsCountLabel.value =
+ PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
+ itemsCount, [itemsCount]);
+ }
+ }
+ },
+
+ // NOT YET USED
+ _updateThumbnail: function PO__updateThumbnail() {
+ var bo = document.getElementById("previewBox").boxObject;
+ var width = bo.width;
+ var height = bo.height;
+
+ var canvas = document.getElementById("itemThumbnail");
+ var ctx = canvas.getContext('2d');
+ var notAvailableText = canvas.getAttribute("notavailabletext");
+ ctx.save();
+ ctx.fillStyle = "-moz-Dialog";
+ ctx.fillRect(0, 0, width, height);
+ ctx.translate(width/2, height/2);
+
+ ctx.fillStyle = "GrayText";
+ ctx.mozTextStyle = "12pt sans serif";
+ var len = ctx.mozMeasureText(notAvailableText);
+ ctx.translate(-len/2, 0);
+ ctx.mozDrawText(notAvailableText);
+ ctx.restore();
+ },
+
+ toggleAdditionalInfoFields: function PO_toggleAdditionalInfoFields() {
+ var infoBox = document.getElementById("infoBox");
+ var infoBoxExpander = document.getElementById("infoBoxExpander");
+ var infoBoxExpanderLabel = document.getElementById("infoBoxExpanderLabel");
+ var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster");
+
+ if (infoBox.getAttribute("minimal") == "true") {
+ infoBox.removeAttribute("minimal");
+ infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("lesslabel");
+ infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("lessaccesskey");
+ infoBoxExpander.className = "expander-up";
+ additionalInfoBroadcaster.removeAttribute("hidden");
+ }
+ else {
+ infoBox.setAttribute("minimal", "true");
+ infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("morelabel");
+ infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("moreaccesskey");
+ infoBoxExpander.className = "expander-down";
+ additionalInfoBroadcaster.setAttribute("hidden", "true");
+ }
+ },
+};
+
+/**
+ * A set of utilities relating to search within Bookmarks and History.
+ */
+var PlacesSearchBox = {
+
+ /**
+ * The Search text field
+ */
+ get searchFilter() {
+ return document.getElementById("searchFilter");
+ },
+
+ /**
+ * Folders to include when searching.
+ */
+ _folders: [],
+ get folders() {
+ if (this._folders.length == 0) {
+ this._folders.push(PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.toolbarFolderId);
+ }
+ return this._folders;
+ },
+ set folders(aFolders) {
+ this._folders = aFolders;
+ return aFolders;
+ },
+
+ /**
+ * Run a search for the specified text, over the collection specified by
+ * the dropdown arrow. The default is all bookmarks, but can be
+ * localized to the active collection.
+ * @param filterString
+ * The text to search for.
+ */
+ search: function PSB_search(filterString) {
+ var PO = PlacesOrganizer;
+ // If the user empties the search box manually, reset it and load all
+ // contents of the current scope.
+ // XXX this might be to jumpy, maybe should search for "", so results
+ // are ungrouped, and search box not reset
+ if (filterString == "") {
+ PO.onPlaceSelected(false);
+ return;
+ }
+
+ let currentView = ContentArea.currentView;
+ let currentOptions = PO.getCurrentOptions();
+
+ // Search according to the current scope, which was set by
+ // PQB_setScope()
+ switch (PlacesSearchBox.filterCollection) {
+ case "bookmarks":
+ currentView.applyFilter(filterString, this.folders);
+ break;
+ case "history":
+ if (currentOptions.queryType != Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ var query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = filterString;
+ var options = currentOptions.clone();
+ // Make sure we're getting uri results.
+ options.resultType = currentOptions.RESULTS_AS_URI;
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
+ options.includeHidden = true;
+ currentView.load([query], options);
+ }
+ else {
+ TelemetryStopwatch.start(HISTORY_LIBRARY_SEARCH_TELEMETRY);
+ currentView.applyFilter(filterString, null, true);
+ TelemetryStopwatch.finish(HISTORY_LIBRARY_SEARCH_TELEMETRY);
+ }
+ break;
+ case "downloads":
+ if (currentView == ContentTree.view) {
+ let query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = filterString;
+ query.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], 1);
+ let options = currentOptions.clone();
+ // Make sure we're getting uri results.
+ options.resultType = currentOptions.RESULTS_AS_URI;
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
+ options.includeHidden = true;
+ currentView.load([query], options);
+ }
+ else {
+ // The new downloads view doesn't use places for searching downloads.
+ currentView.searchTerm = filterString;
+ }
+ break;
+ default:
+ throw "Invalid filterCollection on search";
+ }
+
+ // Update the details panel
+ PlacesOrganizer.updateDetailsPane();
+ },
+
+ /**
+ * Finds across all history, downloads or all bookmarks.
+ */
+ findAll: function PSB_findAll() {
+ switch (this.filterCollection) {
+ case "history":
+ PlacesQueryBuilder.setScope("history");
+ break;
+ case "downloads":
+ PlacesQueryBuilder.setScope("downloads");
+ break;
+ default:
+ PlacesQueryBuilder.setScope("bookmarks");
+ break;
+ }
+ this.focus();
+ },
+
+ /**
+ * Updates the display with the title of the current collection.
+ * @param aTitle
+ * The title of the current collection.
+ */
+ updateCollectionTitle: function PSB_updateCollectionTitle(aTitle) {
+ let title = "";
+ switch (this.filterCollection) {
+ case "history":
+ title = PlacesUIUtils.getString("searchHistory");
+ break;
+ case "downloads":
+ title = PlacesUIUtils.getString("searchDownloads");
+ break;
+ default:
+ title = PlacesUIUtils.getString("searchBookmarks");
+ }
+ this.searchFilter.placeholder = title;
+ },
+
+ /**
+ * Gets/sets the active collection from the dropdown menu.
+ */
+ get filterCollection() {
+ return this.searchFilter.getAttribute("collection");
+ },
+ set filterCollection(collectionName) {
+ if (collectionName == this.filterCollection)
+ return collectionName;
+
+ this.searchFilter.setAttribute("collection", collectionName);
+ this.updateCollectionTitle();
+
+ return collectionName;
+ },
+
+ /**
+ * Focus the search box
+ */
+ focus: function PSB_focus() {
+ this.searchFilter.focus();
+ },
+
+ /**
+ * Set up the gray text in the search bar as the Places View loads.
+ */
+ init: function PSB_init() {
+ this.updateCollectionTitle();
+ },
+
+ /**
+ * Gets or sets the text shown in the Places Search Box
+ */
+ get value() {
+ return this.searchFilter.value;
+ },
+ set value(value) {
+ return this.searchFilter.value = value;
+ },
+};
+
+/**
+ * Functions and data for advanced query builder
+ */
+var PlacesQueryBuilder = {
+
+ queries: [],
+ queryOptions: null,
+
+ /**
+ * Sets the search scope. This can be called when no search is active, and
+ * in that case, when the user does begin a search aScope will be used (see
+ * PSB_search()). If there is an active search, it's performed again to
+ * update the content tree.
+ * @param aScope
+ * The search scope: "bookmarks", "collection", "downloads" or
+ * "history".
+ */
+ setScope: function PQB_setScope(aScope) {
+ // Determine filterCollection, folders, and scopeButtonId based on aScope.
+ var filterCollection;
+ var folders = [];
+ switch (aScope) {
+ case "history":
+ filterCollection = "history";
+ break;
+ case "bookmarks":
+ filterCollection = "bookmarks";
+ folders.push(PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.toolbarFolderId,
+ PlacesUtils.unfiledBookmarksFolderId);
+ break;
+ case "downloads":
+ filterCollection = "downloads";
+ break;
+ default:
+ throw "Invalid search scope";
+ }
+
+ // Update the search box. Re-search if there's an active search.
+ PlacesSearchBox.filterCollection = filterCollection;
+ PlacesSearchBox.folders = folders;
+ var searchStr = PlacesSearchBox.searchFilter.value;
+ if (searchStr)
+ PlacesSearchBox.search(searchStr);
+ }
+};
+
+/**
+ * Population and commands for the View Menu.
+ */
+var ViewMenu = {
+ /**
+ * Removes content generated previously from a menupopup.
+ * @param popup
+ * The popup that contains the previously generated content.
+ * @param startID
+ * The id attribute of an element that is the start of the
+ * dynamically generated region - remove elements after this
+ * item only.
+ * Must be contained by popup. Can be null (in which case the
+ * contents of popup are removed).
+ * @param endID
+ * The id attribute of an element that is the end of the
+ * dynamically generated region - remove elements up to this
+ * item only.
+ * Must be contained by popup. Can be null (in which case all
+ * items until the end of the popup will be removed). Ignored
+ * if startID is null.
+ * @returns The element for the caller to insert new items before,
+ * null if the caller should just append to the popup.
+ */
+ _clean: function VM__clean(popup, startID, endID) {
+ if (endID)
+ NS_ASSERT(startID, "meaningless to have valid endID and null startID");
+ if (startID) {
+ var startElement = document.getElementById(startID);
+ NS_ASSERT(startElement.parentNode ==
+ popup, "startElement is not in popup");
+ NS_ASSERT(startElement,
+ "startID does not correspond to an existing element");
+ var endElement = null;
+ if (endID) {
+ endElement = document.getElementById(endID);
+ NS_ASSERT(endElement.parentNode == popup,
+ "endElement is not in popup");
+ NS_ASSERT(endElement,
+ "endID does not correspond to an existing element");
+ }
+ while (startElement.nextSibling != endElement)
+ popup.removeChild(startElement.nextSibling);
+ return endElement;
+ }
+ while (popup.hasChildNodes()) {
+ popup.removeChild(popup.firstChild);
+ }
+ return null;
+ },
+
+ /**
+ * Fills a menupopup with a list of columns
+ * @param event
+ * The popupshowing event that invoked this function.
+ * @param startID
+ * see _clean
+ * @param endID
+ * see _clean
+ * @param type
+ * the type of the menuitem, e.g. "radio" or "checkbox".
+ * Can be null (no-type).
+ * Checkboxes are checked if the column is visible.
+ * @param propertyPrefix
+ * If propertyPrefix is non-null:
+ * propertyPrefix + column ID + ".label" will be used to get the
+ * localized label string.
+ * propertyPrefix + column ID + ".accesskey" will be used to get the
+ * localized accesskey.
+ * If propertyPrefix is null, the column label is used as label and
+ * no accesskey is assigned.
+ */
+ fillWithColumns: function VM_fillWithColumns(event, startID, endID, type, propertyPrefix) {
+ var popup = event.target;
+ var pivot = this._clean(popup, startID, endID);
+
+ var content = document.getElementById("placeContent");
+ var columns = content.columns;
+ for (var i = 0; i < columns.count; ++i) {
+ var column = columns.getColumnAt(i).element;
+ var menuitem = document.createElement("menuitem");
+ menuitem.id = "menucol_" + column.id;
+ menuitem.column = column;
+ var label = column.getAttribute("label");
+ if (propertyPrefix) {
+ var menuitemPrefix = propertyPrefix;
+ // for string properties, use "name" as the id, instead of "title"
+ // see bug #386287 for details
+ var columnId = column.getAttribute("anonid");
+ menuitemPrefix += columnId == "title" ? "name" : columnId;
+ label = PlacesUIUtils.getString(menuitemPrefix + ".label");
+ var accesskey = PlacesUIUtils.getString(menuitemPrefix + ".accesskey");
+ menuitem.setAttribute("accesskey", accesskey);
+ }
+ menuitem.setAttribute("label", label);
+ if (type == "radio") {
+ menuitem.setAttribute("type", "radio");
+ menuitem.setAttribute("name", "columns");
+ // This column is the sort key. Its item is checked.
+ if (column.getAttribute("sortDirection") != "") {
+ menuitem.setAttribute("checked", "true");
+ }
+ }
+ else if (type == "checkbox") {
+ menuitem.setAttribute("type", "checkbox");
+ // Cannot uncheck the primary column.
+ if (column.getAttribute("primary") == "true")
+ menuitem.setAttribute("disabled", "true");
+ // Items for visible columns are checked.
+ if (!column.hidden)
+ menuitem.setAttribute("checked", "true");
+ }
+ if (pivot)
+ popup.insertBefore(menuitem, pivot);
+ else
+ popup.appendChild(menuitem);
+ }
+ event.stopPropagation();
+ },
+
+ /**
+ * Set up the content of the view menu.
+ */
+ populateSortMenu: function VM_populateSortMenu(event) {
+ this.fillWithColumns(event, "viewUnsorted", "directionSeparator", "radio", "view.sortBy.1.");
+
+ var sortColumn = this._getSortColumn();
+ var viewSortAscending = document.getElementById("viewSortAscending");
+ var viewSortDescending = document.getElementById("viewSortDescending");
+ // We need to remove an existing checked attribute because the unsorted
+ // menu item is not rebuilt every time we open the menu like the others.
+ var viewUnsorted = document.getElementById("viewUnsorted");
+ if (!sortColumn) {
+ viewSortAscending.removeAttribute("checked");
+ viewSortDescending.removeAttribute("checked");
+ viewUnsorted.setAttribute("checked", "true");
+ }
+ else if (sortColumn.getAttribute("sortDirection") == "ascending") {
+ viewSortAscending.setAttribute("checked", "true");
+ viewSortDescending.removeAttribute("checked");
+ viewUnsorted.removeAttribute("checked");
+ }
+ else if (sortColumn.getAttribute("sortDirection") == "descending") {
+ viewSortDescending.setAttribute("checked", "true");
+ viewSortAscending.removeAttribute("checked");
+ viewUnsorted.removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Shows/Hides a tree column.
+ * @param element
+ * The menuitem element for the column
+ */
+ showHideColumn: function VM_showHideColumn(element) {
+ var column = element.column;
+
+ var splitter = column.nextSibling;
+ if (splitter && splitter.localName != "splitter")
+ splitter = null;
+
+ if (element.getAttribute("checked") == "true") {
+ column.setAttribute("hidden", "false");
+ if (splitter)
+ splitter.removeAttribute("hidden");
+ }
+ else {
+ column.setAttribute("hidden", "true");
+ if (splitter)
+ splitter.setAttribute("hidden", "true");
+ }
+ },
+
+ /**
+ * Gets the last column that was sorted.
+ * @returns the currently sorted column, null if there is no sorted column.
+ */
+ _getSortColumn: function VM__getSortColumn() {
+ var content = document.getElementById("placeContent");
+ var cols = content.columns;
+ for (var i = 0; i < cols.count; ++i) {
+ var column = cols.getColumnAt(i).element;
+ var sortDirection = column.getAttribute("sortDirection");
+ if (sortDirection == "ascending" || sortDirection == "descending")
+ return column;
+ }
+ return null;
+ },
+
+ /**
+ * Sorts the view by the specified column.
+ * @param aColumn
+ * The colum that is the sort key. Can be null - the
+ * current sort column or the title column will be used.
+ * @param aDirection
+ * The direction to sort - "ascending" or "descending".
+ * Can be null - the last direction or descending will be used.
+ *
+ * If both aColumnID and aDirection are null, the view will be unsorted.
+ */
+ setSortColumn: function VM_setSortColumn(aColumn, aDirection) {
+ var result = document.getElementById("placeContent").result;
+ if (!aColumn && !aDirection) {
+ result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ return;
+ }
+
+ var columnId;
+ if (aColumn) {
+ columnId = aColumn.getAttribute("anonid");
+ if (!aDirection) {
+ let sortColumn = this._getSortColumn();
+ if (sortColumn)
+ aDirection = sortColumn.getAttribute("sortDirection");
+ }
+ }
+ else {
+ let sortColumn = this._getSortColumn();
+ columnId = sortColumn ? sortColumn.getAttribute("anonid") : "title";
+ }
+
+ // This maps the possible values of columnId (i.e., anonid's of treecols in
+ // placeContent) to the default sortingMode and sortingAnnotation values for
+ // each column.
+ // key: Sort key in the name of one of the
+ // nsINavHistoryQueryOptions.SORT_BY_* constants
+ // dir: Default sort direction to use if none has been specified
+ // anno: The annotation to sort by, if key is "ANNOTATION"
+ var colLookupTable = {
+ title: { key: "TITLE", dir: "ascending" },
+ tags: { key: "TAGS", dir: "ascending" },
+ url: { key: "URI", dir: "ascending" },
+ date: { key: "DATE", dir: "descending" },
+ visitCount: { key: "VISITCOUNT", dir: "descending" },
+ dateAdded: { key: "DATEADDED", dir: "descending" },
+ lastModified: { key: "LASTMODIFIED", dir: "descending" },
+ description: { key: "ANNOTATION",
+ dir: "ascending",
+ anno: PlacesUIUtils.DESCRIPTION_ANNO }
+ };
+
+ // Make sure we have a valid column.
+ if (!colLookupTable.hasOwnProperty(columnId))
+ throw new Error("Invalid column");
+
+ // Use a default sort direction if none has been specified. If aDirection
+ // is invalid, result.sortingMode will be undefined, which has the effect
+ // of unsorting the tree.
+ aDirection = (aDirection || colLookupTable[columnId].dir).toUpperCase();
+
+ var sortConst = "SORT_BY_" + colLookupTable[columnId].key + "_" + aDirection;
+ result.sortingAnnotation = colLookupTable[columnId].anno || "";
+ result.sortingMode = Ci.nsINavHistoryQueryOptions[sortConst];
+ }
+}
+
+var ContentArea = {
+ _specialViews: new Map(),
+
+ init: function CA_init() {
+ this._deck = document.getElementById("placesViewsDeck");
+ this._toolbar = document.getElementById("placesToolbar");
+ ContentTree.init();
+ this._setupView();
+ },
+
+ /**
+ * Gets the content view to be used for loading the given query.
+ * If a custom view was set by setContentViewForQueryString, that
+ * view would be returned, else the default tree view is returned
+ *
+ * @param aQueryString
+ * a query string
+ * @return the view to be used for loading aQueryString.
+ */
+ getContentViewForQueryString:
+ function CA_getContentViewForQueryString(aQueryString) {
+ try {
+ if (this._specialViews.has(aQueryString)) {
+ let { view, options } = this._specialViews.get(aQueryString);
+ if (typeof view == "function") {
+ view = view();
+ this._specialViews.set(aQueryString, { view: view, options: options });
+ }
+ return view;
+ }
+ }
+ catch (ex) {
+ Components.utils.reportError(ex);
+ }
+ return ContentTree.view;
+ },
+
+ /**
+ * Sets a custom view to be used rather than the default places tree
+ * whenever the given query is selected in the left pane.
+ * @param aQueryString
+ * a query string
+ * @param aView
+ * Either the custom view or a function that will return the view
+ * the first (and only) time it's called.
+ * @param [optional] aOptions
+ * Object defining special options for the view.
+ * @see ContentTree.viewOptions for supported options and default values.
+ */
+ setContentViewForQueryString:
+ function CA_setContentViewForQueryString(aQueryString, aView, aOptions) {
+ if (!aQueryString ||
+ typeof aView != "object" && typeof aView != "function")
+ throw new Error("Invalid arguments");
+
+ this._specialViews.set(aQueryString, { view: aView,
+ options: aOptions || {} });
+ },
+
+ get currentView() {
+ return PlacesUIUtils.getViewForNode(this._deck.selectedPanel);
+ },
+ set currentView(aNewView) {
+ let oldView = this.currentView;
+ if (oldView != aNewView) {
+ this._deck.selectedPanel = aNewView.associatedElement;
+
+ // If the content area inactivated view was focused, move focus
+ // to the new view.
+ if (document.activeElement == oldView.associatedElement)
+ aNewView.associatedElement.focus();
+ }
+ return aNewView;
+ },
+
+ get currentPlace() {
+ return this.currentView.place;
+ },
+ set currentPlace(aQueryString) {
+ let oldView = this.currentView;
+ let newView = this.getContentViewForQueryString(aQueryString);
+ newView.place = aQueryString;
+ if (oldView != newView) {
+ oldView.active = false;
+ this.currentView = newView;
+ this._setupView();
+ newView.active = true;
+ }
+ return aQueryString;
+ },
+
+ /**
+ * Applies view options.
+ */
+ _setupView: function CA__setupView() {
+ let options = this.currentViewOptions;
+
+ // showDetailsPane.
+ let detailsDeck = document.getElementById("detailsDeck");
+ detailsDeck.hidden = !options.showDetailsPane;
+
+ // toolbarSet.
+ for (let elt of this._toolbar.childNodes) {
+ // On Windows and Linux the menu buttons are menus wrapped in a menubar.
+ if (elt.id == "placesMenu") {
+ for (let menuElt of elt.childNodes) {
+ menuElt.hidden = !options.toolbarSet.includes(menuElt.id);
+ }
+ }
+ else {
+ elt.hidden = !options.toolbarSet.includes(elt.id);
+ }
+ }
+ },
+
+ /**
+ * Options for the current view.
+ *
+ * @see ContentTree.viewOptions for supported options and default values.
+ */
+ get currentViewOptions() {
+ // Use ContentTree options as default.
+ let viewOptions = ContentTree.viewOptions;
+ if (this._specialViews.has(this.currentPlace)) {
+ let { options } = this._specialViews.get(this.currentPlace);
+ for (let option in options) {
+ viewOptions[option] = options[option];
+ }
+ }
+ return viewOptions;
+ },
+
+ focus: function() {
+ this._deck.selectedPanel.focus();
+ }
+};
+
+var ContentTree = {
+ init: function CT_init() {
+ this._view = document.getElementById("placeContent");
+ },
+
+ get view() {
+ return this._view;
+ },
+
+ get viewOptions() {
+ return Object.seal({
+ showDetailsPane: true,
+ toolbarSet: "back-button, forward-button, organizeButton, viewMenu, maintenanceButton, libraryToolbarSpacer, searchFilter"
+ });
+ },
+
+ openSelectedNode: function CT_openSelectedNode(aEvent) {
+ let view = this.view;
+ PlacesUIUtils.openNodeWithEvent(view.selectedNode, aEvent, view);
+ },
+
+ onClick: function CT_onClick(aEvent) {
+ let node = this.view.selectedNode;
+ if (node) {
+ let doubleClick = aEvent.button == 0 && aEvent.detail == 2;
+ let middleClick = aEvent.button == 1 && aEvent.detail == 1;
+ if (PlacesUtils.nodeIsURI(node) && (doubleClick || middleClick)) {
+ // Open associated uri in the browser.
+ this.openSelectedNode(aEvent);
+ }
+ else if (middleClick && PlacesUtils.nodeIsContainer(node)) {
+ // The command execution function will take care of seeing if the
+ // selection is a folder or a different container type, and will
+ // load its contents in tabs.
+ PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this.view);
+ }
+ }
+ },
+
+ onKeyPress: function CT_onKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN)
+ this.openSelectedNode(aEvent);
+ }
+};
diff --git a/browser/components/places/content/places.xul b/browser/components/places/content/places.xul
new file mode 100644
index 000000000..16c3385cb
--- /dev/null
+++ b/browser/components/places/content/places.xul
@@ -0,0 +1,438 @@
+<?xml version="1.0"?>
+
+# 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/.
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/content/places/organizer.css"?>
+
+<?xml-stylesheet href="chrome://global/skin/"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/organizer.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?>
+
+#ifdef XP_MACOSX
+<?xul-overlay href="chrome://browser/content/macBrowserOverlay.xul"?>
+#else
+<?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+#endif
+
+<!DOCTYPE window [
+<!ENTITY % placesDTD SYSTEM "chrome://browser/locale/places/places.dtd">
+%placesDTD;
+<!ENTITY % editMenuOverlayDTD SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+%editMenuOverlayDTD;
+<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
+%browserDTD;
+]>
+
+<window id="places"
+ title="&places.library.title;"
+ windowtype="Places:Organizer"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="PlacesOrganizer.init();"
+ onunload="PlacesOrganizer.destroy();"
+ width="&places.library.width;" height="&places.library.height;"
+ screenX="10" screenY="10"
+ toggletoolbar="true"
+ persist="width height screenX screenY sizemode">
+
+ <script type="application/javascript"
+ src="chrome://browser/content/places/places.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/utilityOverlay.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/places/editBookmarkOverlay.js"/>
+
+ <stringbundleset id="placesStringSet">
+ <stringbundle id="brandStrings" src="chrome://branding/locale/brand.properties"/>
+ </stringbundleset>
+
+
+#ifdef XP_MACOSX
+#include ../../../base/content/browserMountPoints.inc
+#else
+ <commandset id="editMenuCommands"/>
+ <commandset id="placesCommands"/>
+#endif
+
+ <commandset id="organizerCommandSet">
+ <command id="OrganizerCommand_find:all"
+ oncommand="PlacesSearchBox.findAll();"/>
+ <command id="OrganizerCommand_export"
+ oncommand="PlacesOrganizer.exportBookmarks();"/>
+ <command id="OrganizerCommand_import"
+ oncommand="PlacesOrganizer.importFromFile();"/>
+ <command id="OrganizerCommand_browserImport"
+ oncommand="PlacesOrganizer.importFromBrowser();"/>
+ <command id="OrganizerCommand_backup"
+ oncommand="PlacesOrganizer.backupBookmarks();"/>
+ <command id="OrganizerCommand_restoreFromFile"
+ oncommand="PlacesOrganizer.onRestoreBookmarksFromFile();"/>
+ <command id="OrganizerCommand_search:save"
+ oncommand="PlacesOrganizer.saveSearch();"/>
+ <command id="OrganizerCommand_search:moreCriteria"
+ oncommand="PlacesQueryBuilder.addRow();"/>
+ <command id="OrganizerCommand:Back"
+ oncommand="PlacesOrganizer.back();"/>
+ <command id="OrganizerCommand:Forward"
+ oncommand="PlacesOrganizer.forward();"/>
+ </commandset>
+
+
+ <keyset id="placesOrganizerKeyset">
+ <!-- Instantiation Keys -->
+ <key id="placesKey_close" key="&cmd.close.key;" modifiers="accel"
+ oncommand="close();"/>
+
+ <!-- Command Keys -->
+ <key id="placesKey_find:all"
+ command="OrganizerCommand_find:all"
+ key="&cmd.find.key;"
+ modifiers="accel"/>
+
+ <!-- Back/Forward Keys Support -->
+#ifndef XP_MACOSX
+ <key id="placesKey_goBackKb"
+ keycode="VK_LEFT"
+ command="OrganizerCommand:Back"
+ modifiers="alt"/>
+ <key id="placesKey_goForwardKb"
+ keycode="VK_RIGHT"
+ command="OrganizerCommand:Forward"
+ modifiers="alt"/>
+#else
+ <key id="placesKey_goBackKb"
+ keycode="VK_LEFT"
+ command="OrganizerCommand:Back"
+ modifiers="accel"/>
+ <key id="placesKey_goForwardKb"
+ keycode="VK_RIGHT"
+ command="OrganizerCommand:Forward"
+ modifiers="accel"/>
+#endif
+#ifdef XP_UNIX
+ <key id="placesKey_goBackKb2"
+ key="&goBackCmd.commandKey;"
+ command="OrganizerCommand:Back"
+ modifiers="accel"/>
+ <key id="placesKey_goForwardKb2"
+ key="&goForwardCmd.commandKey;"
+ command="OrganizerCommand:Forward"
+ modifiers="accel"/>
+#endif
+ </keyset>
+
+ <keyset id="editMenuKeys">
+#ifdef XP_MACOSX
+ <key id="key_delete2" keycode="VK_BACK" command="cmd_delete"/>
+#endif
+ </keyset>
+
+ <popupset id="placesPopupset">
+ <menupopup id="placesContext"/>
+ <menupopup id="placesColumnsContext"
+ onpopupshowing="ViewMenu.fillWithColumns(event, null, null, 'checkbox', null);"
+ oncommand="ViewMenu.showHideColumn(event.target); event.stopPropagation();"/>
+ </popupset>
+
+ <toolbox id="placesToolbox">
+ <toolbar class="chromeclass-toolbar" id="placesToolbar" align="center">
+ <toolbarbutton id="back-button"
+ command="OrganizerCommand:Back"
+ tooltiptext="&backButton.tooltip;"
+ disabled="true"/>
+
+ <toolbarbutton id="forward-button"
+ command="OrganizerCommand:Forward"
+ tooltiptext="&forwardButton.tooltip;"
+ disabled="true"/>
+
+#ifdef XP_MACOSX
+ <toolbarbutton type="menu" class="tabbable"
+ onpopupshowing="document.getElementById('placeContent').focus()"
+#else
+ <menubar id="placesMenu">
+ <menu accesskey="&organize.accesskey;" class="menu-iconic"
+#endif
+ id="organizeButton" label="&organize.label;"
+ tooltiptext="&organize.tooltip;">
+ <menupopup id="organizeButtonPopup">
+ <menuitem id="newbookmark"
+ command="placesCmd_new:bookmark"
+ label="&cmd.new_bookmark.label;"
+ accesskey="&cmd.new_bookmark.accesskey;"/>
+ <menuitem id="newfolder"
+ command="placesCmd_new:folder"
+ label="&cmd.new_folder.label;"
+ accesskey="&cmd.new_folder.accesskey;"/>
+ <menuitem id="newseparator"
+ command="placesCmd_new:separator"
+ label="&cmd.new_separator.label;"
+ accesskey="&cmd.new_separator.accesskey;"/>
+
+#ifndef XP_MACOSX
+ <menuseparator id="orgUndoSeparator"/>
+
+ <menuitem id="orgUndo"
+ command="cmd_undo"
+ label="&undoCmd.label;"
+ key="key_undo"
+ accesskey="&undoCmd.accesskey;"/>
+ <menuitem id="orgRedo"
+ command="cmd_redo"
+ label="&redoCmd.label;"
+ key="key_redo"
+ accesskey="&redoCmd.accesskey;"/>
+
+ <menuseparator id="orgCutSeparator"/>
+
+ <menuitem id="orgCut"
+ command="cmd_cut"
+ label="&cutCmd.label;"
+ key="key_cut"
+ accesskey="&cutCmd.accesskey;"
+ selection="separator|link|folder|mixed"/>
+ <menuitem id="orgCopy"
+ command="cmd_copy"
+ label="&copyCmd.label;"
+ key="key_copy"
+ accesskey="&copyCmd.accesskey;"
+ selection="separator|link|folder|mixed"/>
+ <menuitem id="orgPaste"
+ command="cmd_paste"
+ label="&pasteCmd.label;"
+ key="key_paste"
+ accesskey="&pasteCmd.accesskey;"
+ selection="mutable"/>
+ <menuitem id="orgDelete"
+ command="cmd_delete"
+ label="&deleteCmd.label;"
+ key="key_delete"
+ accesskey="&deleteCmd.accesskey;"/>
+
+ <menuseparator id="selectAllSeparator"/>
+
+ <menuitem id="orgSelectAll"
+ command="cmd_selectAll"
+ label="&selectAllCmd.label;"
+ key="key_selectAll"
+ accesskey="&selectAllCmd.accesskey;"/>
+
+#endif
+ <menuseparator id="orgMoveSeparator"/>
+
+ <menuitem id="orgMoveBookmarks"
+ command="placesCmd_moveBookmarks"
+ label="&cmd.moveBookmarks.label;"
+ accesskey="&cmd.moveBookmarks.accesskey;"/>
+#ifdef XP_MACOSX
+ <menuitem id="orgDelete"
+ command="cmd_delete"
+ label="&deleteCmd.label;"
+ key="key_delete"
+ accesskey="&deleteCmd.accesskey;"/>
+#else
+ <menuseparator id="orgCloseSeparator"/>
+
+ <menuitem id="orgClose"
+ key="placesKey_close"
+ label="&file.close.label;"
+ accesskey="&file.close.accesskey;"
+ oncommand="close();"/>
+#endif
+ </menupopup>
+#ifdef XP_MACOSX
+ </toolbarbutton>
+ <toolbarbutton type="menu" class="tabbable"
+#else
+ </menu>
+ <menu accesskey="&views.accesskey;" class="menu-iconic"
+#endif
+ id="viewMenu" label="&views.label;"
+ tooltiptext="&views.tooltip;">
+ <menupopup id="viewMenuPopup">
+
+ <menu id="viewColumns"
+ label="&view.columns.label;" accesskey="&view.columns.accesskey;">
+ <menupopup onpopupshowing="ViewMenu.fillWithColumns(event, null, null, 'checkbox', null);"
+ oncommand="ViewMenu.showHideColumn(event.target); event.stopPropagation();"/>
+ </menu>
+
+ <menu id="viewSort" label="&view.sort.label;"
+ accesskey="&view.sort.accesskey;">
+ <menupopup onpopupshowing="ViewMenu.populateSortMenu(event);"
+ oncommand="ViewMenu.setSortColumn(event.target.column, null);">
+ <menuitem id="viewUnsorted" type="radio" name="columns"
+ label="&view.unsorted.label;" accesskey="&view.unsorted.accesskey;"
+ oncommand="ViewMenu.setSortColumn(null, null);"/>
+ <menuseparator id="directionSeparator"/>
+ <menuitem id="viewSortAscending" type="radio" name="direction"
+ label="&view.sortAscending.label;" accesskey="&view.sortAscending.accesskey;"
+ oncommand="ViewMenu.setSortColumn(null, 'ascending'); event.stopPropagation();"/>
+ <menuitem id="viewSortDescending" type="radio" name="direction"
+ label="&view.sortDescending.label;" accesskey="&view.sortDescending.accesskey;"
+ oncommand="ViewMenu.setSortColumn(null, 'descending'); event.stopPropagation();"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+#ifdef XP_MACOSX
+ </toolbarbutton>
+ <toolbarbutton type="menu" class="tabbable"
+#else
+ </menu>
+ <menu accesskey="&maintenance.accesskey;" class="menu-iconic"
+#endif
+ id="maintenanceButton" label="&maintenance.label;"
+ tooltiptext="&maintenance.tooltip;">
+ <menupopup id="maintenanceButtonPopup">
+ <menuitem id="backupBookmarks"
+ command="OrganizerCommand_backup"
+ label="&cmd.backup.label;"
+ accesskey="&cmd.backup.accesskey;"/>
+ <menu id="fileRestoreMenu" label="&cmd.restore2.label;"
+ accesskey="&cmd.restore2.accesskey;">
+ <menupopup id="fileRestorePopup" onpopupshowing="PlacesOrganizer.populateRestoreMenu();">
+ <menuitem id="restoreFromFile"
+ command="OrganizerCommand_restoreFromFile"
+ label="&cmd.restoreFromFile.label;"
+ accesskey="&cmd.restoreFromFile.accesskey;"/>
+ </menupopup>
+ </menu>
+ <menuseparator/>
+ <menuitem id="fileImport"
+ command="OrganizerCommand_import"
+ label="&importBookmarksFromHTML.label;"
+ accesskey="&importBookmarksFromHTML.accesskey;"/>
+ <menuitem id="fileExport"
+ command="OrganizerCommand_export"
+ label="&exportBookmarksToHTML.label;"
+ accesskey="&exportBookmarksToHTML.accesskey;"/>
+ <menuseparator/>
+ <menuitem id="browserImport"
+ command="OrganizerCommand_browserImport"
+ label="&importOtherBrowser.label;"
+ accesskey="&importOtherBrowser.accesskey;"/>
+ </menupopup>
+#ifdef XP_MACOSX
+ </toolbarbutton>
+#else
+ </menu>
+ </menubar>
+#endif
+
+ <spacer id="libraryToolbarSpacer" flex="1"/>
+
+ <textbox id="searchFilter"
+ clickSelectsAll="true"
+ type="search"
+ aria-controls="placeContent"
+ oncommand="PlacesSearchBox.search(this.value);"
+ collection="bookmarks">
+ </textbox>
+ </toolbar>
+ </toolbox>
+
+ <hbox flex="1" id="placesView">
+ <tree id="placesList"
+ class="plain placesTree"
+ type="places"
+ hidecolumnpicker="true" context="placesContext"
+ onselect="PlacesOrganizer.onPlaceSelected(true);"
+ onclick="PlacesOrganizer.onPlacesListClick(event);"
+ onfocus="PlacesOrganizer.updateDetailsPane(event);"
+ seltype="single"
+ persist="width"
+ width="200"
+ minwidth="100"
+ maxwidth="400">
+ <treecols>
+ <treecol anonid="title" flex="1" primary="true" hideheader="true"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+ <splitter collapse="none" persist="state"></splitter>
+ <vbox id="contentView" flex="4">
+ <deck id="placesViewsDeck"
+ selectedIndex="0"
+ flex="1">
+ <tree id="placeContent"
+ class="plain placesTree"
+ context="placesContext"
+ hidecolumnpicker="true"
+ flex="1"
+ type="places"
+ flatList="true"
+ selectfirstnode="true"
+ enableColumnDrag="true"
+ onfocus="PlacesOrganizer.updateDetailsPane(event)"
+ onselect="PlacesOrganizer.updateDetailsPane(event)"
+ onkeypress="ContentTree.onKeyPress(event);"
+ onopenflatcontainer="PlacesOrganizer.openFlatContainer(aContainer);">
+ <treecols id="placeContentColumns" context="placesColumnsContext">
+ <treecol label="&col.name.label;" id="placesContentTitle" anonid="title" flex="5" primary="true" ordinal="1"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.tags.label;" id="placesContentTags" anonid="tags" flex="2"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.url.label;" id="placesContentUrl" anonid="url" flex="5"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.mostrecentvisit.label;" id="placesContentDate" anonid="date" flex="1" hidden="true"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.visitcount.label;" id="placesContentVisitCount" anonid="visitCount" flex="1" hidden="true"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.description.label;" id="placesContentDescription" anonid="description" flex="1" hidden="true"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.dateadded.label;" id="placesContentDateAdded" anonid="dateAdded" flex="1" hidden="true"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.lastmodified.label;" id="placesContentLastModified" anonid="lastModified" flex="1" hidden="true"
+ persist="width hidden ordinal sortActive sortDirection"/>
+ </treecols>
+ <treechildren flex="1" onclick="ContentTree.onClick(event);"/>
+ </tree>
+ </deck>
+ <deck id="detailsDeck" style="height: 11em;">
+ <vbox id="itemsCountBox" align="center">
+ <spacer flex="3"/>
+ <label id="itemsCountText"/>
+ <spacer flex="1"/>
+ <description id="selectItemDescription">
+ &detailsPane.selectAnItemText.description;
+ </description>
+ <spacer flex="3"/>
+ </vbox>
+ <vbox id="infoBox" minimal="true">
+ <vbox id="editBookmarkPanelContent" flex="1"/>
+ <hbox id="infoBoxExpanderWrapper" align="center">
+
+ <button type="image" id="infoBoxExpander"
+ class="expander-down"
+ oncommand="PlacesOrganizer.toggleAdditionalInfoFields();"
+ observes="paneElementsBroadcaster"/>
+
+ <label id="infoBoxExpanderLabel"
+ lesslabel="&detailsPane.less.label;"
+ lessaccesskey="&detailsPane.less.accesskey;"
+ morelabel="&detailsPane.more.label;"
+ moreaccesskey="&detailsPane.more.accesskey;"
+ value="&detailsPane.more.label;"
+ accesskey="&detailsPane.more.accesskey;"
+ control="infoBoxExpander"/>
+
+ </hbox>
+ </vbox>
+ </deck>
+ </vbox>
+ </hbox>
+</window>
diff --git a/browser/components/places/content/placesOverlay.xul b/browser/components/places/content/placesOverlay.xul
new file mode 100644
index 000000000..512eb923e
--- /dev/null
+++ b/browser/components/places/content/placesOverlay.xul
@@ -0,0 +1,233 @@
+<!-- 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/. -->
+
+<!DOCTYPE overlay [
+<!ENTITY % placesDTD SYSTEM "chrome://browser/locale/places/places.dtd">
+%placesDTD;
+<!ENTITY % editMenuOverlayDTD SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+%editMenuOverlayDTD;
+]>
+
+<overlay id="placesOverlay"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript"
+ src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/utilityOverlay.js"/>
+ <script type="application/javascript"><![CDATA[
+ // TODO: Bug 406371.
+ // A bunch of browser code depends on us defining these, sad but true :(
+ var Cc = Components.classes;
+ var Ci = Components.interfaces;
+ var Cr = Components.results;
+
+ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+ Components.utils.import("resource://gre/modules/Task.jsm");
+ Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+ XPCOMUtils.defineLazyModuleGetter(window,
+ "PlacesUIUtils", "resource:///modules/PlacesUIUtils.jsm");
+ XPCOMUtils.defineLazyModuleGetter(window,
+ "PlacesTransactions", "resource://gre/modules/PlacesTransactions.jsm");
+ ]]></script>
+ <script type="application/javascript"
+ src="chrome://browser/content/places/controller.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/places/treeView.js"/>
+
+ <!-- Bookmarks and history tooltip -->
+ <tooltip id="bhTooltip" noautohide="true"
+ onpopupshowing="return window.top.BookmarksEventHandler.fillInBHTooltip(document, event)">
+ <vbox id="bhTooltipTextBox" flex="1">
+ <label id="bhtTitleText" class="tooltip-label" />
+ <label id="bhtUrlText" crop="center" class="tooltip-label" />
+ </vbox>
+ </tooltip>
+
+ <commandset id="placesCommands"
+ commandupdater="true"
+ events="focus,sort,places"
+ oncommandupdate="goUpdatePlacesCommands();">
+ <command id="placesCmd_open"
+ oncommand="goDoPlacesCommand('placesCmd_open');"/>
+ <command id="placesCmd_open:window"
+ oncommand="goDoPlacesCommand('placesCmd_open:window');"/>
+ <command id="placesCmd_open:privatewindow"
+ oncommand="goDoPlacesCommand('placesCmd_open:privatewindow');"/>
+ <command id="placesCmd_open:tab"
+ oncommand="goDoPlacesCommand('placesCmd_open:tab');"/>
+
+ <command id="placesCmd_new:bookmark"
+ oncommand="goDoPlacesCommand('placesCmd_new:bookmark');"/>
+ <command id="placesCmd_new:folder"
+ oncommand="goDoPlacesCommand('placesCmd_new:folder');"/>
+ <command id="placesCmd_new:separator"
+ oncommand="goDoPlacesCommand('placesCmd_new:separator');"/>
+ <command id="placesCmd_show:info"
+ oncommand="goDoPlacesCommand('placesCmd_show:info');"/>
+ <command id="placesCmd_rename"
+ oncommand="goDoPlacesCommand('placesCmd_show:info');"
+ observes="placesCmd_show:info"/>
+ <command id="placesCmd_reload"
+ oncommand="goDoPlacesCommand('placesCmd_reload');"/>
+ <command id="placesCmd_sortBy:name"
+ oncommand="goDoPlacesCommand('placesCmd_sortBy:name');"/>
+ <command id="placesCmd_moveBookmarks"
+ oncommand="goDoPlacesCommand('placesCmd_moveBookmarks');"/>
+ <command id="placesCmd_deleteDataHost"
+ oncommand="goDoPlacesCommand('placesCmd_deleteDataHost');"/>
+ <command id="placesCmd_createBookmark"
+ oncommand="goDoPlacesCommand('placesCmd_createBookmark');"/>
+
+ <!-- Special versions of cut/copy/paste/delete which check for an open context menu. -->
+ <command id="placesCmd_cut"
+ oncommand="goDoPlacesCommand('placesCmd_cut');"/>
+ <command id="placesCmd_copy"
+ oncommand="goDoPlacesCommand('placesCmd_copy');"/>
+ <command id="placesCmd_paste"
+ oncommand="goDoPlacesCommand('placesCmd_paste');"/>
+ <command id="placesCmd_delete"
+ oncommand="goDoPlacesCommand('placesCmd_delete');"/>
+ </commandset>
+
+ <menupopup id="placesContext"
+ onpopupshowing="this._view = PlacesUIUtils.getViewForNode(document.popupNode);
+ return this._view.buildContextMenu(this);"
+ onpopuphiding="this._view.destroyContextMenu();">
+ <menuitem id="placesContext_open"
+ command="placesCmd_open"
+ label="&cmd.open.label;"
+ accesskey="&cmd.open.accesskey;"
+ default="true"
+ selectiontype="single"
+ selection="link"/>
+ <menuitem id="placesContext_open:newtab"
+ command="placesCmd_open:tab"
+ label="&cmd.open_tab.label;"
+ accesskey="&cmd.open_tab.accesskey;"
+ selectiontype="single"
+ selection="link"/>
+ <menuitem id="placesContext_openContainer:tabs"
+ oncommand="var view = PlacesUIUtils.getViewForNode(document.popupNode);
+ view.controller.openSelectionInTabs(event);"
+ onclick="checkForMiddleClick(this, event);"
+ label="&cmd.open_all_in_tabs.label;"
+ accesskey="&cmd.open_all_in_tabs.accesskey;"
+ selectiontype="single|none"
+ selection="folder|host|query"/>
+ <menuitem id="placesContext_openLinks:tabs"
+ oncommand="var view = PlacesUIUtils.getViewForNode(document.popupNode);
+ view.controller.openSelectionInTabs(event);"
+ onclick="checkForMiddleClick(this, event);"
+ label="&cmd.open_all_in_tabs.label;"
+ accesskey="&cmd.open_all_in_tabs.accesskey;"
+ selectiontype="multiple"
+ selection="link"/>
+ <menuitem id="placesContext_open:newwindow"
+ command="placesCmd_open:window"
+ label="&cmd.open_window.label;"
+ accesskey="&cmd.open_window.accesskey;"
+ selectiontype="single"
+ selection="link"/>
+ <menuitem id="placesContext_open:newprivatewindow"
+ command="placesCmd_open:privatewindow"
+ label="&cmd.open_private_window.label;"
+ accesskey="&cmd.open_private_window.accesskey;"
+ selectiontype="single"
+ selection="link"
+ hideifprivatebrowsing="true"/>
+ <menuseparator id="placesContext_openSeparator"/>
+ <menuitem id="placesContext_new:bookmark"
+ command="placesCmd_new:bookmark"
+ label="&cmd.new_bookmark.label;"
+ accesskey="&cmd.new_bookmark.accesskey;"
+ selectiontype="any"
+ hideifnoinsertionpoint="true"/>
+ <menuitem id="placesContext_new:folder"
+ command="placesCmd_new:folder"
+ label="&cmd.new_folder.label;"
+ accesskey="&cmd.context_new_folder.accesskey;"
+ selectiontype="any"
+ hideifnoinsertionpoint="true"/>
+ <menuitem id="placesContext_new:separator"
+ command="placesCmd_new:separator"
+ label="&cmd.new_separator.label;"
+ accesskey="&cmd.new_separator.accesskey;"
+ closemenu="single"
+ selectiontype="any"
+ hideifnoinsertionpoint="true"/>
+ <menuseparator id="placesContext_newSeparator"/>
+ <menuitem id="placesContext_createBookmark"
+ command="placesCmd_createBookmark"
+ label="&cmd.bookmarkLink.label;"
+ accesskey="&cmd.bookmarkLink.accesskey;"
+ selection="link"
+ forcehideselection="bookmark|tagChild"/>
+ <menuitem id="placesContext_cut"
+ command="placesCmd_cut"
+ label="&cutCmd.label;"
+ accesskey="&cutCmd.accesskey;"
+ closemenu="single"
+ selection="bookmark|folder|separator|query"
+ forcehideselection="tagChild|livemarkChild"/>
+ <menuitem id="placesContext_copy"
+ command="placesCmd_copy"
+ label="&copyCmd.label;"
+ closemenu="single"
+ accesskey="&copyCmd.accesskey;"
+ selection="any"/>
+ <menuitem id="placesContext_paste"
+ command="placesCmd_paste"
+ label="&pasteCmd.label;"
+ closemenu="single"
+ accesskey="&pasteCmd.accesskey;"
+ selectiontype="any"
+ hideifnoinsertionpoint="true"/>
+ <menuseparator id="placesContext_editSeparator"/>
+ <menuitem id="placesContext_delete"
+ command="placesCmd_delete"
+ label="&deleteCmd.label;"
+ accesskey="&deleteCmd.accesskey;"
+ closemenu="single"
+ selection="bookmark|tagChild|folder|query|dynamiccontainer|separator|host"/>
+ <menuitem id="placesContext_delete_history"
+ command="placesCmd_delete"
+ label="&cmd.delete.label;"
+ accesskey="&cmd.delete.accesskey;"
+ closemenu="single"
+ selection="link"
+ forcehideselection="bookmark"/>
+ <menuitem id="placesContext_deleteHost"
+ command="placesCmd_deleteDataHost"
+ label="&cmd.deleteDomainData.label;"
+ accesskey="&cmd.deleteDomainData.accesskey;"
+ closemenu="single"
+ selection="link|host"
+ selectiontype="single"
+ hideifprivatebrowsing="true"
+ forcehideselection="bookmark"/>
+ <menuseparator id="placesContext_deleteSeparator"/>
+ <menuitem id="placesContext_sortBy:name"
+ command="placesCmd_sortBy:name"
+ label="&cmd.sortby_name.label;"
+ accesskey="&cmd.context_sortby_name.accesskey;"
+ closemenu="single"
+ selection="folder"/>
+ <menuitem id="placesContext_reload"
+ command="placesCmd_reload"
+ label="&cmd.reloadLivebookmark.label;"
+ accesskey="&cmd.reloadLivebookmark.accesskey;"
+ closemenu="single"
+ selection="livemark/feedURI"/>
+ <menuseparator id="placesContext_sortSeparator"/>
+ <menuitem id="placesContext_show:info"
+ command="placesCmd_show:info"
+ label="&cmd.properties.label;"
+ accesskey="&cmd.properties.accesskey;"
+ selection="bookmark|folder|query"
+ forcehideselection="livemarkChild"/>
+ </menupopup>
+
+</overlay>
diff --git a/browser/components/places/content/sidebarUtils.js b/browser/components/places/content/sidebarUtils.js
new file mode 100644
index 000000000..96c289741
--- /dev/null
+++ b/browser/components/places/content/sidebarUtils.js
@@ -0,0 +1,106 @@
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
+var SidebarUtils = {
+ handleTreeClick: function SU_handleTreeClick(aTree, aEvent, aGutterSelect) {
+ // right-clicks are not handled here
+ if (aEvent.button == 2)
+ return;
+
+ var tbo = aTree.treeBoxObject;
+ var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY);
+
+ if (cell.row == -1 || cell.childElt == "twisty")
+ return;
+
+ var mouseInGutter = false;
+ if (aGutterSelect) {
+ var rect = tbo.getCoordsForCellItem(cell.row, cell.col, "image");
+ // getCoordsForCellItem returns the x coordinate in logical coordinates
+ // (i.e., starting from the left and right sides in LTR and RTL modes,
+ // respectively.) Therefore, we make sure to exclude the blank area
+ // before the tree item icon (that is, to the left or right of it in
+ // LTR and RTL modes, respectively) from the click target area.
+ var isRTL = window.getComputedStyle(aTree, null).direction == "rtl";
+ if (isRTL)
+ mouseInGutter = aEvent.clientX > rect.x;
+ else
+ mouseInGutter = aEvent.clientX < rect.x;
+ }
+
+ var metaKey = AppConstants.platform === "macosx" ? aEvent.metaKey
+ : aEvent.ctrlKey;
+ var modifKey = metaKey || aEvent.shiftKey;
+ var isContainer = tbo.view.isContainer(cell.row);
+ var openInTabs = isContainer &&
+ (aEvent.button == 1 ||
+ (aEvent.button == 0 && modifKey)) &&
+ PlacesUtils.hasChildURIs(tbo.view.nodeForTreeIndex(cell.row));
+
+ if (aEvent.button == 0 && isContainer && !openInTabs) {
+ tbo.view.toggleOpenState(cell.row);
+ return;
+ }
+ else if (!mouseInGutter && openInTabs &&
+ aEvent.originalTarget.localName == "treechildren") {
+ tbo.view.selection.select(cell.row);
+ PlacesUIUtils.openContainerNodeInTabs(aTree.selectedNode, aEvent, aTree);
+ }
+ else if (!mouseInGutter && !isContainer &&
+ aEvent.originalTarget.localName == "treechildren") {
+ // Clear all other selection since we're loading a link now. We must
+ // do this *before* attempting to load the link since openURL uses
+ // selection as an indication of which link to load.
+ tbo.view.selection.select(cell.row);
+ PlacesUIUtils.openNodeWithEvent(aTree.selectedNode, aEvent, aTree);
+ }
+ },
+
+ handleTreeKeyPress: function SU_handleTreeKeyPress(aEvent) {
+ // XXX Bug 627901: Post Fx4, this method should take a tree parameter.
+ let tree = aEvent.target;
+ let node = tree.selectedNode;
+ if (node) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN)
+ PlacesUIUtils.openNodeWithEvent(node, aEvent, tree);
+ }
+ },
+
+ /**
+ * The following function displays the URL of a node that is being
+ * hovered over.
+ */
+ handleTreeMouseMove: function SU_handleTreeMouseMove(aEvent) {
+ if (aEvent.target.localName != "treechildren")
+ return;
+
+ var tree = aEvent.target.parentNode;
+ var tbo = tree.treeBoxObject;
+ var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY);
+
+ // cell.row is -1 when the mouse is hovering an empty area within the tree.
+ // To avoid showing a URL from a previously hovered node for a currently
+ // hovered non-url node, we must clear the moused-over URL in these cases.
+ if (cell.row != -1) {
+ var node = tree.view.nodeForTreeIndex(cell.row);
+ if (PlacesUtils.nodeIsURI(node))
+ this.setMouseoverURL(node.uri);
+ else
+ this.setMouseoverURL("");
+ }
+ else
+ this.setMouseoverURL("");
+ },
+
+ setMouseoverURL: function SU_setMouseoverURL(aURL) {
+ // When the browser window is closed with an open sidebar, the sidebar
+ // unload event happens after the browser's one. In this case
+ // top.XULBrowserWindow has been nullified already.
+ if (top.XULBrowserWindow) {
+ top.XULBrowserWindow.setOverLink(aURL, null);
+ }
+ }
+};
diff --git a/browser/components/places/content/tree.xml b/browser/components/places/content/tree.xml
new file mode 100644
index 000000000..338ee81f6
--- /dev/null
+++ b/browser/components/places/content/tree.xml
@@ -0,0 +1,801 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<bindings id="placesTreeBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="places-tree" extends="chrome://global/content/bindings/tree.xml#tree">
+ <implementation>
+ <constructor><![CDATA[
+ // Force an initial build.
+ if (this.place)
+ this.place = this.place;
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ // Break the treeviewer->result->treeviewer cycle.
+ // Note: unsetting the result's viewer also unsets
+ // the viewer's reference to our treeBoxObject.
+ var result = this.result;
+ if (result) {
+ result.root.containerOpen = false;
+ }
+
+ // Unregister the controllber before unlinking the view, otherwise it
+ // may still try to update commands on a view with a null result.
+ if (this._controller) {
+ this._controller.terminate();
+ this.controllers.removeController(this._controller);
+ }
+
+ this.view = null;
+ ]]></destructor>
+
+ <property name="controller"
+ readonly="true"
+ onget="return this._controller"/>
+
+ <!-- overriding -->
+ <property name="view">
+ <getter><![CDATA[
+ try {
+ return this.treeBoxObject.view.wrappedJSObject || null;
+ }
+ catch (e) {
+ return null;
+ }
+ ]]></getter>
+ <setter><![CDATA[
+ return this.treeBoxObject.view = val;
+ ]]></setter>
+ </property>
+
+ <property name="associatedElement"
+ readonly="true"
+ onget="return this"/>
+
+ <method name="applyFilter">
+ <parameter name="filterString"/>
+ <parameter name="folderRestrict"/>
+ <parameter name="includeHidden"/>
+ <body><![CDATA[
+ // preserve grouping
+ var queryNode = PlacesUtils.asQuery(this.result.root);
+ var options = queryNode.queryOptions.clone();
+
+ // Make sure we're getting uri results.
+ // We do not yet support searching into grouped queries or into
+ // tag containers, so we must fall to the default case.
+ if (PlacesUtils.nodeIsHistoryContainer(queryNode) ||
+ options.resultType == options.RESULTS_AS_TAG_QUERY ||
+ options.resultType == options.RESULTS_AS_TAG_CONTENTS)
+ options.resultType = options.RESULTS_AS_URI;
+
+ var query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = filterString;
+
+ if (folderRestrict) {
+ query.setFolders(folderRestrict, folderRestrict.length);
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ }
+
+ options.includeHidden = !!includeHidden;
+
+ this.load([query], options);
+ ]]></body>
+ </method>
+
+ <method name="load">
+ <parameter name="queries"/>
+ <parameter name="options"/>
+ <body><![CDATA[
+ let result = PlacesUtils.history
+ .executeQueries(queries, queries.length,
+ options);
+ let callback;
+ if (this.flatList) {
+ let onOpenFlatContainer = this.onOpenFlatContainer;
+ if (onOpenFlatContainer)
+ callback = new Function("aContainer", onOpenFlatContainer);
+ }
+
+ if (!this._controller) {
+ this._controller = new PlacesController(this);
+ this.controllers.appendController(this._controller);
+ }
+
+ let treeView = new PlacesTreeView(this.flatList, callback, this._controller);
+
+ // Observer removal is done within the view itself. When the tree
+ // goes away, treeboxobject calls view.setTree(null), which then
+ // calls removeObserver.
+ result.addObserver(treeView, false);
+ this.view = treeView;
+
+ if (this.getAttribute("selectfirstnode") == "true" && treeView.rowCount > 0) {
+ treeView.selection.select(0);
+ }
+
+ this._cachedInsertionPoint = undefined;
+ ]]></body>
+ </method>
+
+ <property name="flatList">
+ <getter><![CDATA[
+ return this.getAttribute("flatList") == "true";
+ ]]></getter>
+ <setter><![CDATA[
+ if (this.flatList != val) {
+ this.setAttribute("flatList", val);
+ // reload with the last place set
+ if (this.place)
+ this.place = this.place;
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <property name="onOpenFlatContainer">
+ <getter><![CDATA[
+ return this.getAttribute("onopenflatcontainer");
+ ]]></getter>
+ <setter><![CDATA[
+ if (this.onOpenFlatContainer != val) {
+ this.setAttribute("onopenflatcontainer", val);
+ // reload with the last place set
+ if (this.place)
+ this.place = this.place;
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <!--
+ Causes a particular node represented by the specified placeURI to be
+ selected in the tree. All containers above the node in the hierarchy
+ will be opened, so that the node is visible.
+ -->
+ <method name="selectPlaceURI">
+ <parameter name="placeURI"/>
+ <body><![CDATA[
+ // Do nothing if a node matching the given uri is already selected
+ if (this.hasSelection && this.selectedNode.uri == placeURI)
+ return;
+
+ function findNode(container, placeURI, nodesURIChecked) {
+ var containerURI = container.uri;
+ if (containerURI == placeURI)
+ return container;
+ if (nodesURIChecked.includes(containerURI))
+ return null;
+
+ // never check the contents of the same query
+ nodesURIChecked.push(containerURI);
+
+ var wasOpen = container.containerOpen;
+ if (!wasOpen)
+ container.containerOpen = true;
+ for (var i = 0; i < container.childCount; ++i) {
+ var child = container.getChild(i);
+ var childURI = child.uri;
+ if (childURI == placeURI)
+ return child;
+ else if (PlacesUtils.nodeIsContainer(child)) {
+ var nested = findNode(PlacesUtils.asContainer(child), placeURI, nodesURIChecked);
+ if (nested)
+ return nested;
+ }
+ }
+
+ if (!wasOpen)
+ container.containerOpen = false;
+
+ return null;
+ }
+
+ var container = this.result.root;
+ NS_ASSERT(container, "No result, cannot select place URI!");
+ if (!container)
+ return;
+
+ var child = findNode(container, placeURI, []);
+ if (child)
+ this.selectNode(child);
+ else {
+ // If the specified child could not be located, clear the selection
+ var selection = this.view.selection;
+ selection.clearSelection();
+ }
+ ]]></body>
+ </method>
+
+ <!--
+ Causes a particular node to be selected in the tree, resulting in all
+ containers above the node in the hierarchy to be opened, so that the
+ node is visible.
+ -->
+ <method name="selectNode">
+ <parameter name="node"/>
+ <body><![CDATA[
+ var view = this.view;
+
+ var parent = node.parent;
+ if (parent && !parent.containerOpen) {
+ // Build a list of all of the nodes that are the parent of this one
+ // in the result.
+ var parents = [];
+ var root = this.result.root;
+ while (parent && parent != root) {
+ parents.push(parent);
+ parent = parent.parent;
+ }
+
+ // Walk the list backwards (opening from the root of the hierarchy)
+ // opening each folder as we go.
+ for (var i = parents.length - 1; i >= 0; --i) {
+ let index = view.treeIndexForNode(parents[i]);
+ if (index != Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE &&
+ view.isContainer(index) && !view.isContainerOpen(index))
+ view.toggleOpenState(index);
+ }
+ // Select the specified node...
+ }
+
+ let index = view.treeIndexForNode(node);
+ if (index == Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE)
+ return;
+
+ view.selection.select(index);
+ // ... and ensure it's visible, not scrolled off somewhere.
+ this.treeBoxObject.ensureRowIsVisible(index);
+ ]]></body>
+ </method>
+
+ <!-- nsIPlacesView -->
+ <property name="result">
+ <getter><![CDATA[
+ try {
+ return this.view.QueryInterface(Ci.nsINavHistoryResultObserver).result;
+ }
+ catch (e) {
+ return null;
+ }
+ ]]></getter>
+ </property>
+
+ <!-- nsIPlacesView -->
+ <property name="place">
+ <getter><![CDATA[
+ return this.getAttribute("place");
+ ]]></getter>
+ <setter><![CDATA[
+ this.setAttribute("place", val);
+
+ var queriesRef = { };
+ var queryCountRef = { };
+ var optionsRef = { };
+ PlacesUtils.history.queryStringToQueries(val, queriesRef, queryCountRef, optionsRef);
+ if (queryCountRef.value == 0)
+ queriesRef.value = [PlacesUtils.history.getNewQuery()];
+ if (!optionsRef.value)
+ optionsRef.value = PlacesUtils.history.getNewQueryOptions();
+
+ this.load(queriesRef.value, optionsRef.value);
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- nsIPlacesView -->
+ <property name="hasSelection">
+ <getter><![CDATA[
+ return this.view && this.view.selection.count >= 1;
+ ]]></getter>
+ </property>
+
+ <!-- nsIPlacesView -->
+ <property name="selectedNodes">
+ <getter><![CDATA[
+ let nodes = [];
+ if (!this.hasSelection)
+ return nodes;
+
+ let selection = this.view.selection;
+ let rc = selection.getRangeCount();
+ let resultview = this.view;
+ for (let i = 0; i < rc; ++i) {
+ let min = { }, max = { };
+ selection.getRangeAt(i, min, max);
+ for (let j = min.value; j <= max.value; ++j) {
+ nodes.push(resultview.nodeForTreeIndex(j));
+ }
+ }
+ return nodes;
+ ]]></getter>
+ </property>
+
+ <method name="toggleCutNode">
+ <parameter name="aNode"/>
+ <parameter name="aValue"/>
+ <body><![CDATA[
+ this.view.toggleCutNode(aNode, aValue);
+ ]]></body>
+ </method>
+
+ <!-- nsIPlacesView -->
+ <property name="removableSelectionRanges">
+ <getter><![CDATA[
+ // This property exists in addition to selectedNodes because it
+ // encodes selection ranges (which only occur in list views) into
+ // the return value. For each removed range, the index at which items
+ // will be re-inserted upon the remove transaction being performed is
+ // the first index of the range, so that the view updates correctly.
+ //
+ // For example, if we remove rows 2,3,4 and 7,8 from a list, when we
+ // undo that operation, if we insert what was at row 3 at row 3 again,
+ // it will show up _after_ the item that was at row 5. So we need to
+ // insert all items at row 2, and the tree view will update correctly.
+ //
+ // Also, this function collapses the selection to remove redundant
+ // data, e.g. when deleting this selection:
+ //
+ // http://www.foo.com/
+ // (-) Some Folder
+ // http://www.bar.com/
+ //
+ // ... returning http://www.bar.com/ as part of the selection is
+ // redundant because it is implied by removing "Some Folder". We
+ // filter out all such redundancies since some partial amount of
+ // the folder's children may be selected.
+ //
+ let nodes = [];
+ if (!this.hasSelection)
+ return nodes;
+
+ var selection = this.view.selection;
+ var rc = selection.getRangeCount();
+ var resultview = this.view;
+ // This list is kept independently of the range selected (i.e. OUTSIDE
+ // the for loop) since the row index of a container is unique for the
+ // entire view, and we could have some really wacky selection and we
+ // don't want to blow up.
+ var containers = { };
+ for (var i = 0; i < rc; ++i) {
+ var range = [];
+ var min = { }, max = { };
+ selection.getRangeAt(i, min, max);
+
+ for (var j = min.value; j <= max.value; ++j) {
+ if (this.view.isContainer(j))
+ containers[j] = true;
+ if (!(this.view.getParentIndex(j) in containers))
+ range.push(resultview.nodeForTreeIndex(j));
+ }
+ nodes.push(range);
+ }
+ return nodes;
+ ]]></getter>
+ </property>
+
+ <!-- nsIPlacesView -->
+ <property name="draggableSelection"
+ onget="return this.selectedNodes"/>
+
+ <!-- nsIPlacesView -->
+ <property name="selectedNode">
+ <getter><![CDATA[
+ var view = this.view;
+ if (!view || view.selection.count != 1)
+ return null;
+
+ var selection = view.selection;
+ var min = { }, max = { };
+ selection.getRangeAt(0, min, max);
+
+ return this.view.nodeForTreeIndex(min.value);
+ ]]></getter>
+ </property>
+
+ <!-- nsIPlacesView -->
+ <property name="insertionPoint">
+ <getter><![CDATA[
+ // invalidated on selection and focus changes
+ if (this._cachedInsertionPoint !== undefined)
+ return this._cachedInsertionPoint;
+
+ // there is no insertion point for history queries
+ // so bail out now and save a lot of work when updating commands
+ var resultNode = this.result.root;
+ if (PlacesUtils.nodeIsQuery(resultNode) &&
+ PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
+ return this._cachedInsertionPoint = null;
+
+ var orientation = Ci.nsITreeView.DROP_BEFORE;
+ // If there is no selection, insert at the end of the container.
+ if (!this.hasSelection) {
+ var index = this.view.rowCount - 1;
+ this._cachedInsertionPoint =
+ this._getInsertionPoint(index, orientation);
+ return this._cachedInsertionPoint;
+ }
+
+ // This is a two-part process. The first part is determining the drop
+ // orientation.
+ // * The default orientation is to drop _before_ the selected item.
+ // * If the selected item is a container, the default orientation
+ // is to drop _into_ that container.
+ //
+ // Warning: It may be tempting to use tree indexes in this code, but
+ // you must not, since the tree is nested and as your tree
+ // index may change when folders before you are opened and
+ // closed. You must convert your tree index to a node, and
+ // then use getChildIndex to find your absolute index in
+ // the parent container instead.
+ //
+ var resultView = this.view;
+ var selection = resultView.selection;
+ var rc = selection.getRangeCount();
+ var min = { }, max = { };
+ selection.getRangeAt(rc - 1, min, max);
+
+ // If the sole selection is a container, and we are not in
+ // a flatlist, insert into it.
+ // Note that this only applies to _single_ selections,
+ // if the last element within a multi-selection is a
+ // container, insert _adjacent_ to the selection.
+ //
+ // If the sole selection is the bookmarks toolbar folder, we insert
+ // into it even if it is not opened
+ if (selection.count == 1 && resultView.isContainer(max.value) &&
+ !this.flatList)
+ orientation = Ci.nsITreeView.DROP_ON;
+
+ this._cachedInsertionPoint =
+ this._getInsertionPoint(max.value, orientation);
+ return this._cachedInsertionPoint;
+ ]]></getter>
+ </property>
+
+ <method name="_getInsertionPoint">
+ <parameter name="index"/>
+ <parameter name="orientation"/>
+ <body><![CDATA[
+ var result = this.result;
+ var resultview = this.view;
+ var container = result.root;
+ var dropNearItemId = -1;
+ NS_ASSERT(container, "null container");
+ // When there's no selection, assume the container is the container
+ // the view is populated from (i.e. the result's itemId).
+ if (index != -1) {
+ var lastSelected = resultview.nodeForTreeIndex(index);
+ if (resultview.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) {
+ // If the last selected item is an open container, append _into_
+ // it, rather than insert adjacent to it.
+ container = lastSelected;
+ index = -1;
+ }
+ else if (lastSelected.containerOpen &&
+ orientation == Ci.nsITreeView.DROP_AFTER &&
+ lastSelected.hasChildren) {
+ // If the last selected item is an open container and the user is
+ // trying to drag into it as a first item, really insert into it.
+ container = lastSelected;
+ orientation = Ci.nsITreeView.DROP_ON;
+ index = 0;
+ }
+ else {
+ // Use the last-selected node's container.
+ container = lastSelected.parent;
+
+ // See comment in the treeView.js's copy of this method
+ if (!container || !container.containerOpen)
+ return null;
+
+ // Avoid the potentially expensive call to getChildIndex
+ // if we know this container doesn't allow insertion
+ if (PlacesControllerDragHelper.disallowInsertion(container))
+ return null;
+
+ var queryOptions = PlacesUtils.asQuery(result.root).queryOptions;
+ if (queryOptions.sortingMode !=
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
+ // If we are within a sorted view, insert at the end
+ index = -1;
+ }
+ else if (queryOptions.excludeItems ||
+ queryOptions.excludeQueries ||
+ queryOptions.excludeReadOnlyFolders) {
+ // Some item may be invisible, insert near last selected one.
+ // We don't replace index here to avoid requests to the db,
+ // instead it will be calculated later by the controller.
+ index = -1;
+ dropNearItemId = lastSelected.itemId;
+ }
+ else {
+ var lsi = container.getChildIndex(lastSelected);
+ index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
+ }
+ }
+ }
+
+ if (PlacesControllerDragHelper.disallowInsertion(container))
+ return null;
+
+ // TODO (Bug 1160193): properly support dropping on a tag root.
+ let tagName = null;
+ if (PlacesUtils.nodeIsTagQuery(container)) {
+ tagName = container.title;
+ if (!tagName)
+ return null;
+ }
+
+ return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
+ index, orientation,
+ tagName,
+ dropNearItemId);
+ ]]></body>
+ </method>
+
+ <!-- nsIPlacesView -->
+ <method name="selectAll">
+ <body><![CDATA[
+ this.view.selection.selectAll();
+ ]]></body>
+ </method>
+
+ <!-- This method will select the first node in the tree that matches
+ each given item id. It will open any folder nodes that it needs
+ to in order to show the selected items.
+ -->
+ <method name="selectItems">
+ <parameter name="aIDs"/>
+ <parameter name="aOpenContainers"/>
+ <body><![CDATA[
+ // Never open containers in flat lists.
+ if (this.flatList)
+ aOpenContainers = false;
+ // By default, we do search and select within containers which were
+ // closed (note that containers in which nodes were not found are
+ // closed).
+ if (aOpenContainers === undefined)
+ aOpenContainers = true;
+
+ var ids = aIDs; // don't manipulate the caller's array
+
+ // Array of nodes found by findNodes which are to be selected
+ var nodes = [];
+
+ // Array of nodes found by findNodes which should be opened
+ var nodesToOpen = [];
+
+ // A set of GUIDs of container-nodes that were previously searched,
+ // and thus shouldn't be searched again. This is empty at the initial
+ // start of the recursion and gets filled in as the recursion
+ // progresses.
+ var checkedGuidsSet = new Set();
+
+ /**
+ * Recursively search through a node's children for items
+ * with the given IDs. When a matching item is found, remove its ID
+ * from the IDs array, and add the found node to the nodes dictionary.
+ *
+ * NOTE: This method will leave open any node that had matching items
+ * in its subtree.
+ */
+ function findNodes(node) {
+ var foundOne = false;
+ // See if node matches an ID we wanted; add to results.
+ // For simple folder queries, check both itemId and the concrete
+ // item id.
+ var index = ids.indexOf(node.itemId);
+ if (index == -1 &&
+ node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT)
+ index = ids.indexOf(PlacesUtils.asQuery(node).folderItemId);
+
+ if (index != -1) {
+ nodes.push(node);
+ foundOne = true;
+ ids.splice(index, 1);
+ }
+
+ var concreteGuid = PlacesUtils.getConcreteItemGuid(node);
+ if (ids.length == 0 || !PlacesUtils.nodeIsContainer(node) ||
+ checkedGuidsSet.has(concreteGuid))
+ return foundOne;
+
+ // Only follow a query if it has been been explicitly opened by the caller.
+ let shouldOpen = aOpenContainers && PlacesUtils.nodeIsFolder(node);
+ PlacesUtils.asContainer(node);
+ if (!node.containerOpen && !shouldOpen)
+ return foundOne;
+
+ checkedGuidsSet.add(concreteGuid);
+
+ // Remember the beginning state so that we can re-close
+ // this node if we don't find any additional results here.
+ var previousOpenness = node.containerOpen;
+ node.containerOpen = true;
+ for (var child = 0; child < node.childCount && ids.length > 0;
+ child++) {
+ var childNode = node.getChild(child);
+ var found = findNodes(childNode);
+ if (!foundOne)
+ foundOne = found;
+ }
+
+ // If we didn't find any additional matches in this node's
+ // subtree, revert the node to its previous openness.
+ if (foundOne)
+ nodesToOpen.unshift(node);
+ node.containerOpen = previousOpenness;
+ return foundOne;
+ }
+
+ // Disable notifications while looking for nodes.
+ let result = this.result;
+ let didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true
+ try {
+ findNodes(this.result.root);
+ }
+ finally {
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+
+ // For all the nodes we've found, highlight the corresponding
+ // index in the tree.
+ var resultview = this.view;
+ var selection = this.view.selection;
+ selection.selectEventsSuppressed = true;
+ selection.clearSelection();
+ // Open nodes containing found items
+ for (let i = 0; i < nodesToOpen.length; i++) {
+ nodesToOpen[i].containerOpen = true;
+ }
+ for (let i = 0; i < nodes.length; i++) {
+ var index = resultview.treeIndexForNode(nodes[i]);
+ if (index == Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE)
+ continue;
+ selection.rangedSelect(index, index, true);
+ }
+ selection.selectEventsSuppressed = false;
+ ]]></body>
+ </method>
+
+ <field name="_contextMenuShown">false</field>
+
+ <method name="buildContextMenu">
+ <parameter name="aPopup"/>
+ <body><![CDATA[
+ this._contextMenuShown = true;
+ return this.controller.buildContextMenu(aPopup);
+ ]]></body>
+ </method>
+
+ <method name="destroyContextMenu">
+ <parameter name="aPopup"/>
+ this._contextMenuShown = false;
+ <body/>
+ </method>
+
+ <property name="ownerWindow"
+ readonly="true"
+ onget="return window;"/>
+
+ <field name="_active">true</field>
+ <property name="active"
+ onget="return this._active"
+ onset="return this._active = val"/>
+
+ </implementation>
+ <handlers>
+ <handler event="focus"><![CDATA[
+ this._cachedInsertionPoint = undefined;
+
+ // See select handler. We need the sidebar's places commandset to be
+ // updated as well
+ document.commandDispatcher.updateCommands("focus");
+ ]]></handler>
+ <handler event="select"><![CDATA[
+ this._cachedInsertionPoint = undefined;
+
+ // This additional complexity is here for the sidebars
+ var win = window;
+ while (true) {
+ win.document.commandDispatcher.updateCommands("focus");
+ if (win == window.top)
+ break;
+
+ win = win.parent;
+ }
+ ]]></handler>
+
+ <handler event="dragstart"><![CDATA[
+ if (event.target.localName != "treechildren")
+ return;
+
+ let nodes = this.selectedNodes;
+ for (let i = 0; i < nodes.length; i++) {
+ let node = nodes[i];
+
+ // Disallow dragging the root node of a tree.
+ if (!node.parent) {
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
+ // If this node is child of a readonly container (e.g. a livemark)
+ // or cannot be moved, we must force a copy.
+ if (!PlacesControllerDragHelper.canMoveNode(node)) {
+ event.dataTransfer.effectAllowed = "copyLink";
+ break;
+ }
+ }
+
+ this._controller.setDataTransfer(event);
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragover"><![CDATA[
+ if (event.target.localName != "treechildren")
+ return;
+
+ let cell = this.treeBoxObject.getCellAt(event.clientX, event.clientY);
+ let node = cell.row != -1 ?
+ this.view.nodeForTreeIndex(cell.row) :
+ this.result.root;
+ // cache the dropTarget for the view
+ PlacesControllerDragHelper.currentDropTarget = node;
+
+ // We have to calculate the orientation since view.canDrop will use
+ // it and we want to be consistent with the dropfeedback.
+ let tbo = this.treeBoxObject;
+ let rowHeight = tbo.rowHeight;
+ let eventY = event.clientY - tbo.treeBody.boxObject.y -
+ rowHeight * (cell.row - tbo.getFirstVisibleRow());
+
+ let orientation = Ci.nsITreeView.DROP_BEFORE;
+
+ if (cell.row == -1) {
+ // If the row is not valid we try to insert inside the resultNode.
+ orientation = Ci.nsITreeView.DROP_ON;
+ }
+ else if (PlacesUtils.nodeIsContainer(node) &&
+ eventY > rowHeight * 0.75) {
+ // If we are below the 75% of a container the treeview we try
+ // to drop after the node.
+ orientation = Ci.nsITreeView.DROP_AFTER;
+ }
+ else if (PlacesUtils.nodeIsContainer(node) &&
+ eventY > rowHeight * 0.25) {
+ // If we are below the 25% of a container the treeview we try
+ // to drop inside the node.
+ orientation = Ci.nsITreeView.DROP_ON;
+ }
+
+ if (!this.view.canDrop(cell.row, orientation, event.dataTransfer))
+ return;
+
+ event.preventDefault();
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragend"><![CDATA[
+ PlacesControllerDragHelper.currentDropTarget = null;
+ ]]></handler>
+
+ </handlers>
+ </binding>
+
+</bindings>
diff --git a/browser/components/places/content/treeView.js b/browser/components/places/content/treeView.js
new file mode 100644
index 000000000..5baf3a21f
--- /dev/null
+++ b/browser/components/places/content/treeView.js
@@ -0,0 +1,1726 @@
+/* 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/. */
+
+Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
+
+const PTV_interfaces = [Ci.nsITreeView,
+ Ci.nsINavHistoryResultObserver,
+ Ci.nsINavHistoryResultTreeViewer,
+ Ci.nsISupportsWeakReference];
+
+function PlacesTreeView(aFlatList, aOnOpenFlatContainer, aController) {
+ this._tree = null;
+ this._result = null;
+ this._selection = null;
+ this._rootNode = null;
+ this._rows = [];
+ this._flatList = aFlatList;
+ this._openContainerCallback = aOnOpenFlatContainer;
+ this._controller = aController;
+}
+
+PlacesTreeView.prototype = {
+ get wrappedJSObject() {
+ return this;
+ },
+
+ __xulStore: null,
+ get _xulStore() {
+ if (!this.__xulStore) {
+ this.__xulStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore);
+ }
+ return this.__xulStore;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI(PTV_interfaces),
+
+ // Bug 761494:
+ // ----------
+ // Some addons use methods from nsINavHistoryResultObserver and
+ // nsINavHistoryResultTreeViewer, without QIing to these interfaces first.
+ // That's not a problem when the view is retrieved through the
+ // <tree>.view getter (which returns the wrappedJSObject of this object),
+ // it raises an issue when the view retrieved through the treeBoxObject.view
+ // getter. Thus, to avoid breaking addons, the interfaces are prefetched.
+ classInfo: XPCOMUtils.generateCI({ interfaces: PTV_interfaces }),
+
+ /**
+ * This is called once both the result and the tree are set.
+ */
+ _finishInit: function PTV__finishInit() {
+ let selection = this.selection;
+ if (selection)
+ selection.selectEventsSuppressed = true;
+
+ if (!this._rootNode.containerOpen) {
+ // This triggers containerStateChanged which then builds the visible
+ // section.
+ this._rootNode.containerOpen = true;
+ }
+ else
+ this.invalidateContainer(this._rootNode);
+
+ // "Activate" the sorting column and update commands.
+ this.sortingChanged(this._result.sortingMode);
+
+ if (selection)
+ selection.selectEventsSuppressed = false;
+ },
+
+ /**
+ * Plain Container: container result nodes which may never include sub
+ * hierarchies.
+ *
+ * When the rows array is constructed, we don't set the children of plain
+ * containers. Instead, we keep placeholders for these children. We then
+ * build these children lazily as the tree asks us for information about each
+ * row. Luckily, the tree doesn't ask about rows outside the visible area.
+ *
+ * @see _getNodeForRow and _getRowForNode for the actual magic.
+ *
+ * @note It's guaranteed that all containers are listed in the rows
+ * elements array. It's also guaranteed that separators (if they're not
+ * filtered, see below) are listed in the visible elements array, because
+ * bookmark folders are never built lazily, as described above.
+ *
+ * @param aContainer
+ * A container result node.
+ *
+ * @return true if aContainer is a plain container, false otherwise.
+ */
+ _isPlainContainer: function PTV__isPlainContainer(aContainer) {
+ // Livemarks are always plain containers.
+ if (this._controller.hasCachedLivemarkInfo(aContainer))
+ return true;
+
+ // We don't know enough about non-query containers.
+ if (!(aContainer instanceof Ci.nsINavHistoryQueryResultNode))
+ return false;
+
+ switch (aContainer.queryOptions.resultType) {
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY:
+ return false;
+ }
+
+ // If it's a folder, it's not a plain container.
+ let nodeType = aContainer.type;
+ return nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER &&
+ nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
+ },
+
+ /**
+ * Gets the row number for a given node. Assumes that the given node is
+ * visible (i.e. it's not an obsolete node).
+ *
+ * @param aNode
+ * A result node. Do not pass an obsolete node, or any
+ * node which isn't supposed to be in the tree (e.g. separators in
+ * sorted trees).
+ * @param [optional] aForceBuild
+ * @see _isPlainContainer.
+ * If true, the row will be computed even if the node still isn't set
+ * in our rows array.
+ * @param [optional] aParentRow
+ * The row of aNode's parent. Ignored for the root node.
+ * @param [optional] aNodeIndex
+ * The index of aNode in its parent. Only used if aParentRow is
+ * set too.
+ *
+ * @throws if aNode is invisible.
+ * @note If aParentRow and aNodeIndex are passed and parent is a plain
+ * container, this method will just return a calculated row value, without
+ * making assumptions on existence of the node at that position.
+ * @return aNode's row if it's in the rows list or if aForceBuild is set, -1
+ * otherwise.
+ */
+ _getRowForNode:
+ function PTV__getRowForNode(aNode, aForceBuild, aParentRow, aNodeIndex) {
+ if (aNode == this._rootNode)
+ throw new Error("The root node is never visible");
+
+ // A node is removed form the view either if it has no parent or if its
+ // root-ancestor is not the root node (in which case that's the node
+ // for which nodeRemoved was called).
+ let ancestors = Array.from(PlacesUtils.nodeAncestors(aNode));
+ if (ancestors.length == 0 ||
+ ancestors[ancestors.length - 1] != this._rootNode) {
+ throw new Error("Removed node passed to _getRowForNode");
+ }
+
+ // Ensure that the entire chain is open, otherwise that node is invisible.
+ for (let ancestor of ancestors) {
+ if (!ancestor.containerOpen)
+ throw new Error("Invisible node passed to _getRowForNode");
+ }
+
+ // Non-plain containers are initially built with their contents.
+ let parent = aNode.parent;
+ let parentIsPlain = this._isPlainContainer(parent);
+ if (!parentIsPlain) {
+ if (parent == this._rootNode)
+ return this._rows.indexOf(aNode);
+
+ return this._rows.indexOf(aNode, aParentRow);
+ }
+
+ let row = -1;
+ let useNodeIndex = typeof(aNodeIndex) == "number";
+ if (parent == this._rootNode) {
+ row = useNodeIndex ? aNodeIndex : this._rootNode.getChildIndex(aNode);
+ }
+ else if (useNodeIndex && typeof(aParentRow) == "number") {
+ // If we have both the row of the parent node, and the node's index, we
+ // can avoid searching the rows array if the parent is a plain container.
+ row = aParentRow + aNodeIndex + 1;
+ }
+ else {
+ // Look for the node in the nodes array. Start the search at the parent
+ // row. If the parent row isn't passed, we'll pass undefined to indexOf,
+ // which is fine.
+ row = this._rows.indexOf(aNode, aParentRow);
+ if (row == -1 && aForceBuild) {
+ let parentRow = typeof(aParentRow) == "number" ? aParentRow
+ : this._getRowForNode(parent);
+ row = parentRow + parent.getChildIndex(aNode) + 1;
+ }
+ }
+
+ if (row != -1)
+ this._rows[row] = aNode;
+
+ return row;
+ },
+
+ /**
+ * Given a row, finds and returns the parent details of the associated node.
+ *
+ * @param aChildRow
+ * Row number.
+ * @return [parentNode, parentRow]
+ */
+ _getParentByChildRow: function PTV__getParentByChildRow(aChildRow) {
+ let node = this._getNodeForRow(aChildRow);
+ let parent = (node === null) ? this._rootNode : node.parent;
+
+ // The root node is never visible
+ if (parent == this._rootNode)
+ return [this._rootNode, -1];
+
+ let parentRow = this._rows.lastIndexOf(parent, aChildRow - 1);
+ return [parent, parentRow];
+ },
+
+ /**
+ * Gets the node at a given row.
+ */
+ _getNodeForRow: function PTV__getNodeForRow(aRow) {
+ if (aRow < 0) {
+ return null;
+ }
+
+ let node = this._rows[aRow];
+ if (node !== undefined)
+ return node;
+
+ // Find the nearest node.
+ let rowNode, row;
+ for (let i = aRow - 1; i >= 0 && rowNode === undefined; i--) {
+ rowNode = this._rows[i];
+ row = i;
+ }
+
+ // If there's no container prior to the given row, it's a child of
+ // the root node (remember: all containers are listed in the rows array).
+ if (!rowNode)
+ return this._rows[aRow] = this._rootNode.getChild(aRow);
+
+ // Unset elements may exist only in plain containers. Thus, if the nearest
+ // node is a container, it's the row's parent, otherwise, it's a sibling.
+ if (rowNode instanceof Ci.nsINavHistoryContainerResultNode)
+ return this._rows[aRow] = rowNode.getChild(aRow - row - 1);
+
+ let [parent, parentRow] = this._getParentByChildRow(row);
+ return this._rows[aRow] = parent.getChild(aRow - parentRow - 1);
+ },
+
+ /**
+ * This takes a container and recursively appends our rows array per its
+ * contents. Assumes that the rows arrays has no rows for the given
+ * container.
+ *
+ * @param [in] aContainer
+ * A container result node.
+ * @param [in] aFirstChildRow
+ * The first row at which nodes may be inserted to the row array.
+ * In other words, that's aContainer's row + 1.
+ * @param [out] aToOpen
+ * An array of containers to open once the build is done.
+ *
+ * @return the number of rows which were inserted.
+ */
+ _buildVisibleSection:
+ function PTV__buildVisibleSection(aContainer, aFirstChildRow, aToOpen)
+ {
+ // There's nothing to do if the container is closed.
+ if (!aContainer.containerOpen)
+ return 0;
+
+ // Inserting the new elements into the rows array in one shot (by
+ // Array.prototype.concat) is faster than resizing the array (by splice) on each loop
+ // iteration.
+ let cc = aContainer.childCount;
+ let newElements = new Array(cc);
+ this._rows = this._rows.splice(0, aFirstChildRow)
+ .concat(newElements, this._rows);
+
+ if (this._isPlainContainer(aContainer))
+ return cc;
+
+ let sortingMode = this._result.sortingMode;
+
+ let rowsInserted = 0;
+ for (let i = 0; i < cc; i++) {
+ let curChild = aContainer.getChild(i);
+ let curChildType = curChild.type;
+
+ let row = aFirstChildRow + rowsInserted;
+
+ // Don't display separators when sorted.
+ if (curChildType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ if (sortingMode != Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
+ // Remove the element for the filtered separator.
+ // Notice that the rows array was initially resized to include all
+ // children.
+ this._rows.splice(row, 1);
+ continue;
+ }
+ }
+
+ this._rows[row] = curChild;
+ rowsInserted++;
+
+ // Recursively do containers.
+ if (!this._flatList &&
+ curChild instanceof Ci.nsINavHistoryContainerResultNode &&
+ !this._controller.hasCachedLivemarkInfo(curChild)) {
+ let uri = curChild.uri;
+ let isopen = false;
+
+ if (uri) {
+ let val = this._xulStore.getValue(document.documentURI, uri, "open");
+ isopen = (val == "true");
+ }
+
+ if (isopen != curChild.containerOpen)
+ aToOpen.push(curChild);
+ else if (curChild.containerOpen && curChild.childCount > 0)
+ rowsInserted += this._buildVisibleSection(curChild, row + 1, aToOpen);
+ }
+ }
+
+ return rowsInserted;
+ },
+
+ /**
+ * This counts how many rows a node takes in the tree. For containers it
+ * will count the node itself plus any child node following it.
+ */
+ _countVisibleRowsForNodeAtRow:
+ function PTV__countVisibleRowsForNodeAtRow(aNodeRow) {
+ let node = this._rows[aNodeRow];
+
+ // If it's not listed yet, we know that it's a leaf node (instanceof also
+ // null-checks).
+ if (!(node instanceof Ci.nsINavHistoryContainerResultNode))
+ return 1;
+
+ let outerLevel = node.indentLevel;
+ for (let i = aNodeRow + 1; i < this._rows.length; i++) {
+ let rowNode = this._rows[i];
+ if (rowNode && rowNode.indentLevel <= outerLevel)
+ return i - aNodeRow;
+ }
+
+ // This node plus its children take up the bottom of the list.
+ return this._rows.length - aNodeRow;
+ },
+
+ _getSelectedNodesInRange:
+ function PTV__getSelectedNodesInRange(aFirstRow, aLastRow) {
+ let selection = this.selection;
+ let rc = selection.getRangeCount();
+ if (rc == 0)
+ return [];
+
+ // The visible-area borders are needed for checking whether a
+ // selected row is also visible.
+ let firstVisibleRow = this._tree.getFirstVisibleRow();
+ let lastVisibleRow = this._tree.getLastVisibleRow();
+
+ let nodesInfo = [];
+ for (let rangeIndex = 0; rangeIndex < rc; rangeIndex++) {
+ let min = { }, max = { };
+ selection.getRangeAt(rangeIndex, min, max);
+
+ // If this range does not overlap the replaced chunk, we don't need to
+ // persist the selection.
+ if (max.value < aFirstRow || min.value > aLastRow)
+ continue;
+
+ let firstRow = Math.max(min.value, aFirstRow);
+ let lastRow = Math.min(max.value, aLastRow);
+ for (let i = firstRow; i <= lastRow; i++) {
+ nodesInfo.push({
+ node: this._rows[i],
+ oldRow: i,
+ wasVisible: i >= firstVisibleRow && i <= lastVisibleRow
+ });
+ }
+ }
+
+ return nodesInfo;
+ },
+
+ /**
+ * Tries to find an equivalent node for a node which was removed. We first
+ * look for the original node, in case it was just relocated. Then, if we
+ * that node was not found, we look for a node that has the same itemId, uri
+ * and time values.
+ *
+ * @param aUpdatedContainer
+ * An ancestor of the node which was removed. It does not have to be
+ * its direct parent.
+ * @param aOldNode
+ * The node which was removed.
+ *
+ * @return the row number of an equivalent node for aOldOne, if one was
+ * found, -1 otherwise.
+ */
+ _getNewRowForRemovedNode:
+ function PTV__getNewRowForRemovedNode(aUpdatedContainer, aOldNode) {
+ let parent = aOldNode.parent;
+ if (parent) {
+ // If the node's parent is still set, the node is not obsolete
+ // and we should just find out its new position.
+ // However, if any of the node's ancestor is closed, the node is
+ // invisible.
+ let ancestors = PlacesUtils.nodeAncestors(aOldNode);
+ for (let ancestor of ancestors) {
+ if (!ancestor.containerOpen)
+ return -1;
+ }
+
+ return this._getRowForNode(aOldNode, true);
+ }
+
+ // There's a broken edge case here.
+ // If a visit appears in two queries, and the second one was
+ // the old node, we'll select the first one after refresh. There's
+ // nothing we could do about that, because aOldNode.parent is
+ // gone by the time invalidateContainer is called.
+ let newNode = aUpdatedContainer.findNodeByDetails(aOldNode.uri,
+ aOldNode.time,
+ aOldNode.itemId,
+ true);
+ if (!newNode)
+ return -1;
+
+ return this._getRowForNode(newNode, true);
+ },
+
+ /**
+ * Restores a given selection state as near as possible to the original
+ * selection state.
+ *
+ * @param aNodesInfo
+ * The persisted selection state as returned by
+ * _getSelectedNodesInRange.
+ * @param aUpdatedContainer
+ * The container which was updated.
+ */
+ _restoreSelection:
+ function PTV__restoreSelection(aNodesInfo, aUpdatedContainer) {
+ if (aNodesInfo.length == 0)
+ return;
+
+ let selection = this.selection;
+
+ // Attempt to ensure that previously-visible selection will be visible
+ // if it's re-selected. However, we can only ensure that for one row.
+ let scrollToRow = -1;
+ for (let i = 0; i < aNodesInfo.length; i++) {
+ let nodeInfo = aNodesInfo[i];
+ let row = this._getNewRowForRemovedNode(aUpdatedContainer,
+ nodeInfo.node);
+ // Select the found node, if any.
+ if (row != -1) {
+ selection.rangedSelect(row, row, true);
+ if (nodeInfo.wasVisible && scrollToRow == -1)
+ scrollToRow = row;
+ }
+ }
+
+ // If only one node was previously selected and there's no selection now,
+ // select the node at its old row, if any.
+ if (aNodesInfo.length == 1 && selection.count == 0) {
+ let row = Math.min(aNodesInfo[0].oldRow, this._rows.length - 1);
+ if (row != -1) {
+ selection.rangedSelect(row, row, true);
+ if (aNodesInfo[0].wasVisible && scrollToRow == -1)
+ scrollToRow = aNodesInfo[0].oldRow;
+ }
+ }
+
+ if (scrollToRow != -1)
+ this._tree.ensureRowIsVisible(scrollToRow);
+ },
+
+ _convertPRTimeToString: function PTV__convertPRTimeToString(aTime) {
+ const MS_PER_MINUTE = 60000;
+ const MS_PER_DAY = 86400000;
+ let timeMs = aTime / 1000; // PRTime is in microseconds
+
+ // Date is calculated starting from midnight, so the modulo with a day are
+ // milliseconds from today's midnight.
+ // getTimezoneOffset corrects that based on local time, notice midnight
+ // can have a different offset during DST-change days.
+ let dateObj = new Date();
+ let now = dateObj.getTime() - dateObj.getTimezoneOffset() * MS_PER_MINUTE;
+ let midnight = now - (now % MS_PER_DAY);
+ midnight += new Date(midnight).getTimezoneOffset() * MS_PER_MINUTE;
+
+ let timeObj = new Date(timeMs);
+ return timeMs >= midnight ? this._todayFormatter.format(timeObj)
+ : this._dateFormatter.format(timeObj);
+ },
+
+ // We use a different formatter for times within the current day,
+ // so we cache both a "today" formatter and a general date formatter.
+ __todayFormatter: null,
+ get _todayFormatter() {
+ if (!this.__todayFormatter) {
+ const locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIXULChromeRegistry)
+ .getSelectedLocale("global", true);
+ const dtOptions = { hour: 'numeric', minute: 'numeric' };
+ this.__todayFormatter = new Intl.DateTimeFormat(locale, dtOptions);
+ }
+ return this.__todayFormatter;
+ },
+
+ __dateFormatter: null,
+ get _dateFormatter() {
+ if (!this.__dateFormatter) {
+ const locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIXULChromeRegistry)
+ .getSelectedLocale("global", true);
+ const dtOptions = { year: 'numeric', month: 'numeric', day: 'numeric',
+ hour: 'numeric', minute: 'numeric' };
+ this.__dateFormatter = new Intl.DateTimeFormat(locale, dtOptions);
+ }
+ return this.__dateFormatter;
+ },
+
+ COLUMN_TYPE_UNKNOWN: 0,
+ COLUMN_TYPE_TITLE: 1,
+ COLUMN_TYPE_URI: 2,
+ COLUMN_TYPE_DATE: 3,
+ COLUMN_TYPE_VISITCOUNT: 4,
+ COLUMN_TYPE_DESCRIPTION: 5,
+ COLUMN_TYPE_DATEADDED: 6,
+ COLUMN_TYPE_LASTMODIFIED: 7,
+ COLUMN_TYPE_TAGS: 8,
+
+ _getColumnType: function PTV__getColumnType(aColumn) {
+ let columnType = aColumn.element.getAttribute("anonid") || aColumn.id;
+
+ switch (columnType) {
+ case "title":
+ return this.COLUMN_TYPE_TITLE;
+ case "url":
+ return this.COLUMN_TYPE_URI;
+ case "date":
+ return this.COLUMN_TYPE_DATE;
+ case "visitCount":
+ return this.COLUMN_TYPE_VISITCOUNT;
+ case "description":
+ return this.COLUMN_TYPE_DESCRIPTION;
+ case "dateAdded":
+ return this.COLUMN_TYPE_DATEADDED;
+ case "lastModified":
+ return this.COLUMN_TYPE_LASTMODIFIED;
+ case "tags":
+ return this.COLUMN_TYPE_TAGS;
+ }
+ return this.COLUMN_TYPE_UNKNOWN;
+ },
+
+ _sortTypeToColumnType: function PTV__sortTypeToColumnType(aSortType) {
+ switch (aSortType) {
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING:
+ return [this.COLUMN_TYPE_TITLE, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING:
+ return [this.COLUMN_TYPE_TITLE, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING:
+ return [this.COLUMN_TYPE_DATE, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING:
+ return [this.COLUMN_TYPE_DATE, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_ASCENDING:
+ return [this.COLUMN_TYPE_URI, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_DESCENDING:
+ return [this.COLUMN_TYPE_URI, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_ASCENDING:
+ return [this.COLUMN_TYPE_VISITCOUNT, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING:
+ return [this.COLUMN_TYPE_VISITCOUNT, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING:
+ if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
+ return [this.COLUMN_TYPE_DESCRIPTION, false];
+ break;
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING:
+ if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
+ return [this.COLUMN_TYPE_DESCRIPTION, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING:
+ return [this.COLUMN_TYPE_DATEADDED, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING:
+ return [this.COLUMN_TYPE_DATEADDED, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_ASCENDING:
+ return [this.COLUMN_TYPE_LASTMODIFIED, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING:
+ return [this.COLUMN_TYPE_LASTMODIFIED, true];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_ASCENDING:
+ return [this.COLUMN_TYPE_TAGS, false];
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_DESCENDING:
+ return [this.COLUMN_TYPE_TAGS, true];
+ }
+ return [this.COLUMN_TYPE_UNKNOWN, false];
+ },
+
+ // nsINavHistoryResultObserver
+ nodeInserted: function PTV_nodeInserted(aParentNode, aNode, aNewIndex) {
+ NS_ASSERT(this._result, "Got a notification but have no result!");
+ if (!this._tree || !this._result)
+ return;
+
+ // Bail out for hidden separators.
+ if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
+ return;
+
+ let parentRow;
+ if (aParentNode != this._rootNode) {
+ parentRow = this._getRowForNode(aParentNode);
+
+ // Update parent when inserting the first item, since twisty has changed.
+ if (aParentNode.childCount == 1)
+ this._tree.invalidateRow(parentRow);
+ }
+
+ // Compute the new row number of the node.
+ let row = -1;
+ let cc = aParentNode.childCount;
+ if (aNewIndex == 0 || this._isPlainContainer(aParentNode) || cc == 0) {
+ // We don't need to worry about sub hierarchies of the parent node
+ // if it's a plain container, or if the new node is its first child.
+ if (aParentNode == this._rootNode)
+ row = aNewIndex;
+ else
+ row = parentRow + aNewIndex + 1;
+ }
+ else {
+ // Here, we try to find the next visible element in the child list so we
+ // can set the new visible index to be right before that. Note that we
+ // have to search down instead of up, because some siblings could have
+ // children themselves that would be in the way.
+ let separatorsAreHidden = PlacesUtils.nodeIsSeparator(aNode) &&
+ this.isSorted();
+ for (let i = aNewIndex + 1; i < cc; i++) {
+ let node = aParentNode.getChild(i);
+ if (!separatorsAreHidden || PlacesUtils.nodeIsSeparator(node)) {
+ // The children have not been shifted so the next item will have what
+ // should be our index.
+ row = this._getRowForNode(node, false, parentRow, i);
+ break;
+ }
+ }
+ if (row < 0) {
+ // At the end of the child list without finding a visible sibling. This
+ // is a little harder because we don't know how many rows the last item
+ // in our list takes up (it could be a container with many children).
+ let prevChild = aParentNode.getChild(aNewIndex - 1);
+ let prevIndex = this._getRowForNode(prevChild, false, parentRow,
+ aNewIndex - 1);
+ row = prevIndex + this._countVisibleRowsForNodeAtRow(prevIndex);
+ }
+ }
+
+ this._rows.splice(row, 0, aNode);
+ this._tree.rowCountChanged(row, 1);
+
+ if (PlacesUtils.nodeIsContainer(aNode) &&
+ PlacesUtils.asContainer(aNode).containerOpen) {
+ this.invalidateContainer(aNode);
+ }
+ },
+
+ /**
+ * THIS FUNCTION DOES NOT HANDLE cases where a collapsed node is being
+ * removed but the node it is collapsed with is not being removed (this then
+ * just swap out the removee with its collapsing partner). The only time
+ * when we really remove things is when deleting URIs, which will apply to
+ * all collapsees. This function is called sometimes when resorting items.
+ * However, we won't do this when sorted by date because dates will never
+ * change for visits, and date sorting is the only time things are collapsed.
+ */
+ nodeRemoved: function PTV_nodeRemoved(aParentNode, aNode, aOldIndex) {
+ NS_ASSERT(this._result, "Got a notification but have no result!");
+ if (!this._tree || !this._result)
+ return;
+
+ // XXX bug 517701: We don't know what to do when the root node is removed.
+ if (aNode == this._rootNode)
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+
+ // Bail out for hidden separators.
+ if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
+ return;
+
+ let parentRow = aParentNode == this._rootNode ?
+ undefined : this._getRowForNode(aParentNode, true);
+ let oldRow = this._getRowForNode(aNode, true, parentRow, aOldIndex);
+ if (oldRow < 0)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ // If the node was exclusively selected, the node next to it will be
+ // selected.
+ let selectNext = false;
+ let selection = this.selection;
+ if (selection.getRangeCount() == 1) {
+ let min = { }, max = { };
+ selection.getRangeAt(0, min, max);
+ if (min.value == max.value &&
+ this.nodeForTreeIndex(min.value) == aNode)
+ selectNext = true;
+ }
+
+ // Remove the node and its children, if any.
+ let count = this._countVisibleRowsForNodeAtRow(oldRow);
+ this._rows.splice(oldRow, count);
+ this._tree.rowCountChanged(oldRow, -count);
+
+ // Redraw the parent if its twisty state has changed.
+ if (aParentNode != this._rootNode && !aParentNode.hasChildren) {
+ let parentRow = oldRow - 1;
+ this._tree.invalidateRow(parentRow);
+ }
+
+ // Restore selection if the node was exclusively selected.
+ if (!selectNext)
+ return;
+
+ // Restore selection.
+ let rowToSelect = Math.min(oldRow, this._rows.length - 1);
+ if (rowToSelect != -1)
+ this.selection.rangedSelect(rowToSelect, rowToSelect, true);
+ },
+
+ nodeMoved:
+ function PTV_nodeMoved(aNode, aOldParent, aOldIndex, aNewParent, aNewIndex) {
+ NS_ASSERT(this._result, "Got a notification but have no result!");
+ if (!this._tree || !this._result)
+ return;
+
+ // Bail out for hidden separators.
+ if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
+ return;
+
+ // Note that at this point the node has already been moved by the backend,
+ // so we must give hints to _getRowForNode to get the old row position.
+ let oldParentRow = aOldParent == this._rootNode ?
+ undefined : this._getRowForNode(aOldParent, true);
+ let oldRow = this._getRowForNode(aNode, true, oldParentRow, aOldIndex);
+ if (oldRow < 0)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ // If this node is a container it could take up more than one row.
+ let count = this._countVisibleRowsForNodeAtRow(oldRow);
+
+ // Persist selection state.
+ let nodesToReselect =
+ this._getSelectedNodesInRange(oldRow, oldRow + count);
+ if (nodesToReselect.length > 0)
+ this.selection.selectEventsSuppressed = true;
+
+ // Redraw the parent if its twisty state has changed.
+ if (aOldParent != this._rootNode && !aOldParent.hasChildren) {
+ let parentRow = oldRow - 1;
+ this._tree.invalidateRow(parentRow);
+ }
+
+ // Remove node and its children, if any, from the old position.
+ this._rows.splice(oldRow, count);
+ this._tree.rowCountChanged(oldRow, -count);
+
+ // Insert the node into the new position.
+ this.nodeInserted(aNewParent, aNode, aNewIndex);
+
+ // Restore selection.
+ if (nodesToReselect.length > 0) {
+ this._restoreSelection(nodesToReselect, aNewParent);
+ this.selection.selectEventsSuppressed = false;
+ }
+ },
+
+ _invalidateCellValue: function PTV__invalidateCellValue(aNode,
+ aColumnType) {
+ NS_ASSERT(this._result, "Got a notification but have no result!");
+ if (!this._tree || !this._result)
+ return;
+
+ // Nothing to do for the root node.
+ if (aNode == this._rootNode)
+ return;
+
+ let row = this._getRowForNode(aNode);
+ if (row == -1)
+ return;
+
+ let column = this._findColumnByType(aColumnType);
+ if (column && !column.element.hidden)
+ this._tree.invalidateCell(row, column);
+
+ // Last modified time is altered for almost all node changes.
+ if (aColumnType != this.COLUMN_TYPE_LASTMODIFIED) {
+ let lastModifiedColumn =
+ this._findColumnByType(this.COLUMN_TYPE_LASTMODIFIED);
+ if (lastModifiedColumn && !lastModifiedColumn.hidden)
+ this._tree.invalidateCell(row, lastModifiedColumn);
+ }
+ },
+
+ _populateLivemarkContainer: function PTV__populateLivemarkContainer(aNode) {
+ PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
+ .then(aLivemark => {
+ let placesNode = aNode;
+ // Need to check containerOpen since getLivemark is async.
+ if (!placesNode.containerOpen)
+ return;
+
+ let children = aLivemark.getNodesForContainer(placesNode);
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+ this.nodeInserted(placesNode, child, i);
+ }
+ }, Components.utils.reportError);
+ },
+
+ nodeTitleChanged: function PTV_nodeTitleChanged(aNode, aNewTitle) {
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
+ },
+
+ nodeURIChanged: function PTV_nodeURIChanged(aNode, aNewURI) {
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_URI);
+ },
+
+ nodeIconChanged: function PTV_nodeIconChanged(aNode) {
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
+ },
+
+ nodeHistoryDetailsChanged:
+ function PTV_nodeHistoryDetailsChanged(aNode, aUpdatedVisitDate,
+ aUpdatedVisitCount) {
+ if (aNode.parent && this._controller.hasCachedLivemarkInfo(aNode.parent)) {
+ // Find the node in the parent.
+ let parentRow = this._flatList ? 0 : this._getRowForNode(aNode.parent);
+ for (let i = parentRow; i < this._rows.length; i++) {
+ let child = this.nodeForTreeIndex(i);
+ if (child.uri == aNode.uri) {
+ this._cellProperties.delete(child);
+ this._invalidateCellValue(child, this.COLUMN_TYPE_TITLE);
+ break;
+ }
+ }
+ return;
+ }
+
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATE);
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_VISITCOUNT);
+ },
+
+ nodeTagsChanged: function PTV_nodeTagsChanged(aNode) {
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_TAGS);
+ },
+
+ nodeKeywordChanged(aNode, aNewKeyword) {},
+
+ nodeAnnotationChanged: function PTV_nodeAnnotationChanged(aNode, aAnno) {
+ if (aAnno == PlacesUIUtils.DESCRIPTION_ANNO) {
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_DESCRIPTION);
+ }
+ else if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
+ PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
+ .then(aLivemark => {
+ this._controller.cacheLivemarkInfo(aNode, aLivemark);
+ let properties = this._cellProperties.get(aNode);
+ this._cellProperties.set(aNode, properties += " livemark");
+ // The livemark attribute is set as a cell property on the title cell.
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
+ }, Components.utils.reportError);
+ }
+ },
+
+ nodeDateAddedChanged: function PTV_nodeDateAddedChanged(aNode, aNewValue) {
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATEADDED);
+ },
+
+ nodeLastModifiedChanged:
+ function PTV_nodeLastModifiedChanged(aNode, aNewValue) {
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_LASTMODIFIED);
+ },
+
+ containerStateChanged:
+ function PTV_containerStateChanged(aNode, aOldState, aNewState) {
+ this.invalidateContainer(aNode);
+
+ if (PlacesUtils.nodeIsFolder(aNode) ||
+ (this._flatList && aNode == this._rootNode)) {
+ let queryOptions = PlacesUtils.asQuery(this._rootNode).queryOptions;
+ if (queryOptions.excludeItems) {
+ return;
+ }
+ if (aNode.itemId != -1) { // run when there's a valid node id
+ PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
+ .then(aLivemark => {
+ let shouldInvalidate =
+ !this._controller.hasCachedLivemarkInfo(aNode);
+ this._controller.cacheLivemarkInfo(aNode, aLivemark);
+ if (aNewState == Components.interfaces.nsINavHistoryContainerResultNode.STATE_OPENED) {
+ aLivemark.registerForUpdates(aNode, this);
+ // Prioritize the current livemark.
+ aLivemark.reload();
+ PlacesUtils.livemarks.reloadLivemarks();
+ if (shouldInvalidate)
+ this.invalidateContainer(aNode);
+ }
+ else {
+ aLivemark.unregisterForUpdates(aNode);
+ }
+ }, () => undefined);
+ }
+ }
+ },
+
+ invalidateContainer: function PTV_invalidateContainer(aContainer) {
+ NS_ASSERT(this._result, "Need to have a result to update");
+ if (!this._tree)
+ return;
+
+ let startReplacement, replaceCount;
+ if (aContainer == this._rootNode) {
+ startReplacement = 0;
+ replaceCount = this._rows.length;
+
+ // If the root node is now closed, the tree is empty.
+ if (!this._rootNode.containerOpen) {
+ this._rows = [];
+ if (replaceCount)
+ this._tree.rowCountChanged(startReplacement, -replaceCount);
+
+ return;
+ }
+ }
+ else {
+ // Update the twisty state.
+ let row = this._getRowForNode(aContainer);
+ this._tree.invalidateRow(row);
+
+ // We don't replace the container node itself, so we should decrease the
+ // replaceCount by 1.
+ startReplacement = row + 1;
+ replaceCount = this._countVisibleRowsForNodeAtRow(row) - 1;
+ }
+
+ // Persist selection state.
+ let nodesToReselect =
+ this._getSelectedNodesInRange(startReplacement,
+ startReplacement + replaceCount);
+
+ // Now update the number of elements.
+ this.selection.selectEventsSuppressed = true;
+
+ // First remove the old elements
+ this._rows.splice(startReplacement, replaceCount);
+
+ // If the container is now closed, we're done.
+ if (!aContainer.containerOpen) {
+ let oldSelectionCount = this.selection.count;
+ if (replaceCount)
+ this._tree.rowCountChanged(startReplacement, -replaceCount);
+
+ // Select the row next to the closed container if any of its
+ // children were selected, and nothing else is selected.
+ if (nodesToReselect.length > 0 &&
+ nodesToReselect.length == oldSelectionCount) {
+ this.selection.rangedSelect(startReplacement, startReplacement, true);
+ this._tree.ensureRowIsVisible(startReplacement);
+ }
+
+ this.selection.selectEventsSuppressed = false;
+ return;
+ }
+
+ // Otherwise, start a batch first.
+ this._tree.beginUpdateBatch();
+ if (replaceCount)
+ this._tree.rowCountChanged(startReplacement, -replaceCount);
+
+ let toOpenElements = [];
+ let elementsAddedCount = this._buildVisibleSection(aContainer,
+ startReplacement,
+ toOpenElements);
+ if (elementsAddedCount)
+ this._tree.rowCountChanged(startReplacement, elementsAddedCount);
+
+ if (!this._flatList) {
+ // Now, open any containers that were persisted.
+ for (let i = 0; i < toOpenElements.length; i++) {
+ let item = toOpenElements[i];
+ let parent = item.parent;
+
+ // Avoid recursively opening containers.
+ while (parent) {
+ if (parent.uri == item.uri)
+ break;
+ parent = parent.parent;
+ }
+
+ // If we don't have a parent, we made it all the way to the root
+ // and didn't find a match, so we can open our item.
+ if (!parent && !item.containerOpen)
+ item.containerOpen = true;
+ }
+ }
+
+ if (this._controller.hasCachedLivemarkInfo(aContainer)) {
+ let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
+ if (!queryOptions.excludeItems) {
+ this._populateLivemarkContainer(aContainer);
+ }
+ }
+
+ this._tree.endUpdateBatch();
+
+ // Restore selection.
+ this._restoreSelection(nodesToReselect, aContainer);
+ this.selection.selectEventsSuppressed = false;
+ },
+
+ _columns: [],
+ _findColumnByType: function PTV__findColumnByType(aColumnType) {
+ if (this._columns[aColumnType])
+ return this._columns[aColumnType];
+
+ let columns = this._tree.columns;
+ let colCount = columns.count;
+ for (let i = 0; i < colCount; i++) {
+ let column = columns.getColumnAt(i);
+ let columnType = this._getColumnType(column);
+ this._columns[columnType] = column;
+ if (columnType == aColumnType)
+ return column;
+ }
+
+ // That's completely valid. Most of our trees actually include just the
+ // title column.
+ return null;
+ },
+
+ sortingChanged: function PTV__sortingChanged(aSortingMode) {
+ if (!this._tree || !this._result)
+ return;
+
+ // Depending on the sort mode, certain commands may be disabled.
+ window.updateCommands("sort");
+
+ let columns = this._tree.columns;
+
+ // Clear old sorting indicator.
+ let sortedColumn = columns.getSortedColumn();
+ if (sortedColumn)
+ sortedColumn.element.removeAttribute("sortDirection");
+
+ // Set new sorting indicator by looking through all columns for ours.
+ if (aSortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_NONE)
+ return;
+
+ let [desiredColumn, desiredIsDescending] =
+ this._sortTypeToColumnType(aSortingMode);
+ let column = this._findColumnByType(desiredColumn);
+ if (column) {
+ let sortDir = desiredIsDescending ? "descending" : "ascending";
+ column.element.setAttribute("sortDirection", sortDir);
+ }
+ },
+
+ _inBatchMode: false,
+ batching: function PTV__batching(aToggleMode) {
+ if (this._inBatchMode != aToggleMode) {
+ this._inBatchMode = this.selection.selectEventsSuppressed = aToggleMode;
+ if (this._inBatchMode) {
+ this._tree.beginUpdateBatch();
+ }
+ else {
+ this._tree.endUpdateBatch();
+ }
+ }
+ },
+
+ get result() {
+ return this._result;
+ },
+ set result(val) {
+ if (this._result) {
+ this._result.removeObserver(this);
+ this._rootNode.containerOpen = false;
+ }
+
+ if (val) {
+ this._result = val;
+ this._rootNode = this._result.root;
+ this._cellProperties = new Map();
+ this._cuttingNodes = new Set();
+ }
+ else if (this._result) {
+ delete this._result;
+ delete this._rootNode;
+ delete this._cellProperties;
+ delete this._cuttingNodes;
+ }
+
+ // If the tree is not set yet, setTree will call finishInit.
+ if (this._tree && val)
+ this._finishInit();
+
+ return val;
+ },
+
+ nodeForTreeIndex: function PTV_nodeForTreeIndex(aIndex) {
+ if (aIndex > this._rows.length)
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ return this._getNodeForRow(aIndex);
+ },
+
+ treeIndexForNode: function PTV_treeNodeForIndex(aNode) {
+ // The API allows passing invisible nodes.
+ try {
+ return this._getRowForNode(aNode, true);
+ }
+ catch (ex) { }
+
+ return Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE;
+ },
+
+ // nsITreeView
+ get rowCount() {
+ return this._rows.length;
+ },
+ get selection() {
+ return this._selection;
+ },
+ set selection(val) {
+ this._selection = val;
+ },
+
+ getRowProperties: function() { return ""; },
+
+ getCellProperties:
+ function PTV_getCellProperties(aRow, aColumn) {
+ // for anonid-trees, we need to add the column-type manually
+ var props = "";
+ let columnType = aColumn.element.getAttribute("anonid");
+ if (columnType)
+ props += columnType;
+ else
+ columnType = aColumn.id;
+
+ // Set the "ltr" property on url cells
+ if (columnType == "url")
+ props += " ltr";
+
+ if (columnType != "title")
+ return props;
+
+ let node = this._getNodeForRow(aRow);
+
+ if (this._cuttingNodes.has(node)) {
+ props += " cutting";
+ }
+
+ let properties = this._cellProperties.get(node);
+ if (properties === undefined) {
+ properties = "";
+ let itemId = node.itemId;
+ let nodeType = node.type;
+ if (PlacesUtils.containerTypes.includes(nodeType)) {
+ if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
+ properties += " query";
+ if (PlacesUtils.nodeIsTagQuery(node))
+ properties += " tagContainer";
+ else if (PlacesUtils.nodeIsDay(node))
+ properties += " dayContainer";
+ else if (PlacesUtils.nodeIsHost(node))
+ properties += " hostContainer";
+ }
+ else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
+ nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
+ if (this._controller.hasCachedLivemarkInfo(node)) {
+ properties += " livemark";
+ }
+ else {
+ PlacesUtils.livemarks.getLivemark({ id: node.itemId })
+ .then(aLivemark => {
+ this._controller.cacheLivemarkInfo(node, aLivemark);
+ let props = this._cellProperties.get(node);
+ this._cellProperties.set(node, props += " livemark");
+ // The livemark attribute is set as a cell property on the title cell.
+ this._invalidateCellValue(node, this.COLUMN_TYPE_TITLE);
+ }, () => undefined);
+ }
+ }
+
+ if (itemId != -1) {
+ let queryName = PlacesUIUtils.getLeftPaneQueryNameFromId(itemId);
+ if (queryName)
+ properties += " OrganizerQuery_" + queryName;
+ }
+ }
+ else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR)
+ properties += " separator";
+ else if (PlacesUtils.nodeIsURI(node)) {
+ properties += " " + PlacesUIUtils.guessUrlSchemeForUI(node.uri);
+
+ if (this._controller.hasCachedLivemarkInfo(node.parent)) {
+ properties += " livemarkItem";
+ if (node.accessCount) {
+ properties += " visited";
+ }
+ }
+ }
+
+ this._cellProperties.set(node, properties);
+ }
+
+ return props + " " + properties;
+ },
+
+ getColumnProperties: function(aColumn) { return ""; },
+
+ isContainer: function PTV_isContainer(aRow) {
+ // Only leaf nodes aren't listed in the rows array.
+ let node = this._rows[aRow];
+ if (node === undefined)
+ return false;
+
+ if (PlacesUtils.nodeIsContainer(node)) {
+ // Flat-lists may ignore expandQueries and other query options when
+ // they are asked to open a container.
+ if (this._flatList)
+ return true;
+
+ // treat non-expandable childless queries as non-containers
+ if (PlacesUtils.nodeIsQuery(node)) {
+ let parent = node.parent;
+ if ((PlacesUtils.nodeIsQuery(parent) ||
+ PlacesUtils.nodeIsFolder(parent)) &&
+ !PlacesUtils.asQuery(node).hasChildren)
+ return PlacesUtils.asQuery(parent).queryOptions.expandQueries;
+ }
+ return true;
+ }
+ return false;
+ },
+
+ isContainerOpen: function PTV_isContainerOpen(aRow) {
+ if (this._flatList)
+ return false;
+
+ // All containers are listed in the rows array.
+ return this._rows[aRow].containerOpen;
+ },
+
+ isContainerEmpty: function PTV_isContainerEmpty(aRow) {
+ if (this._flatList)
+ return true;
+
+ let node = this._rows[aRow];
+ if (this._controller.hasCachedLivemarkInfo(node)) {
+ let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
+ return queryOptions.excludeItems;
+ }
+
+ // All containers are listed in the rows array.
+ return !node.hasChildren;
+ },
+
+ isSeparator: function PTV_isSeparator(aRow) {
+ // All separators are listed in the rows array.
+ let node = this._rows[aRow];
+ return node && PlacesUtils.nodeIsSeparator(node);
+ },
+
+ isSorted: function PTV_isSorted() {
+ return this._result.sortingMode !=
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ },
+
+ canDrop: function PTV_canDrop(aRow, aOrientation, aDataTransfer) {
+ if (!this._result)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ // Drop position into a sorted treeview would be wrong.
+ if (this.isSorted())
+ return false;
+
+ let ip = this._getInsertionPoint(aRow, aOrientation);
+ return ip && PlacesControllerDragHelper.canDrop(ip, aDataTransfer);
+ },
+
+ _getInsertionPoint: function PTV__getInsertionPoint(index, orientation) {
+ let container = this._result.root;
+ let dropNearItemId = -1;
+ // When there's no selection, assume the container is the container
+ // the view is populated from (i.e. the result's itemId).
+ if (index != -1) {
+ let lastSelected = this.nodeForTreeIndex(index);
+ if (this.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) {
+ // If the last selected item is an open container, append _into_
+ // it, rather than insert adjacent to it.
+ container = lastSelected;
+ index = -1;
+ }
+ else if (lastSelected.containerOpen &&
+ orientation == Ci.nsITreeView.DROP_AFTER &&
+ lastSelected.hasChildren) {
+ // If the last selected node is an open container and the user is
+ // trying to drag into it as a first node, really insert into it.
+ container = lastSelected;
+ orientation = Ci.nsITreeView.DROP_ON;
+ index = 0;
+ }
+ else {
+ // Use the last-selected node's container.
+ container = lastSelected.parent;
+
+ // During its Drag & Drop operation, the tree code closes-and-opens
+ // containers very often (part of the XUL "spring-loaded folders"
+ // implementation). And in certain cases, we may reach a closed
+ // container here. However, we can simply bail out when this happens,
+ // because we would then be back here in less than a millisecond, when
+ // the container had been reopened.
+ if (!container || !container.containerOpen)
+ return null;
+
+ // Avoid the potentially expensive call to getChildIndex
+ // if we know this container doesn't allow insertion.
+ if (PlacesControllerDragHelper.disallowInsertion(container))
+ return null;
+
+ let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
+ if (queryOptions.sortingMode !=
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
+ // If we are within a sorted view, insert at the end.
+ index = -1;
+ }
+ else if (queryOptions.excludeItems ||
+ queryOptions.excludeQueries ||
+ queryOptions.excludeReadOnlyFolders) {
+ // Some item may be invisible, insert near last selected one.
+ // We don't replace index here to avoid requests to the db,
+ // instead it will be calculated later by the controller.
+ index = -1;
+ dropNearItemId = lastSelected.itemId;
+ }
+ else {
+ let lsi = container.getChildIndex(lastSelected);
+ index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
+ }
+ }
+ }
+
+ if (PlacesControllerDragHelper.disallowInsertion(container))
+ return null;
+
+ // TODO (Bug 1160193): properly support dropping on a tag root.
+ let tagName = null;
+ if (PlacesUtils.nodeIsTagQuery(container)) {
+ tagName = container.title;
+ if (!tagName)
+ return null;
+ }
+
+ return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
+ index, orientation,
+ tagName,
+ dropNearItemId);
+ },
+
+ drop: function PTV_drop(aRow, aOrientation, aDataTransfer) {
+ // We are responsible for translating the |index| and |orientation|
+ // parameters into a container id and index within the container,
+ // since this information is specific to the tree view.
+ let ip = this._getInsertionPoint(aRow, aOrientation);
+ if (ip) {
+ PlacesControllerDragHelper.onDrop(ip, aDataTransfer)
+ .then(null, Components.utils.reportError);
+ }
+
+ PlacesControllerDragHelper.currentDropTarget = null;
+ },
+
+ getParentIndex: function PTV_getParentIndex(aRow) {
+ let [, parentRow] = this._getParentByChildRow(aRow);
+ return parentRow;
+ },
+
+ hasNextSibling: function PTV_hasNextSibling(aRow, aAfterIndex) {
+ if (aRow == this._rows.length - 1) {
+ // The last row has no sibling.
+ return false;
+ }
+
+ let node = this._rows[aRow];
+ if (node === undefined || this._isPlainContainer(node.parent)) {
+ // The node is a child of a plain container.
+ // If the next row is either unset or has the same parent,
+ // it's a sibling.
+ let nextNode = this._rows[aRow + 1];
+ return (nextNode == undefined || nextNode.parent == node.parent);
+ }
+
+ let thisLevel = node.indentLevel;
+ for (let i = aAfterIndex + 1; i < this._rows.length; ++i) {
+ let rowNode = this._getNodeForRow(i);
+ let nextLevel = rowNode.indentLevel;
+ if (nextLevel == thisLevel)
+ return true;
+ if (nextLevel < thisLevel)
+ break;
+ }
+
+ return false;
+ },
+
+ getLevel: function(aRow) {
+ return this._getNodeForRow(aRow).indentLevel;
+ },
+
+ getImageSrc: function PTV_getImageSrc(aRow, aColumn) {
+ // Only the title column has an image.
+ if (this._getColumnType(aColumn) != this.COLUMN_TYPE_TITLE)
+ return "";
+
+ let node = this._getNodeForRow(aRow);
+ return node.icon;
+ },
+
+ getProgressMode: function(aRow, aColumn) { },
+ getCellValue: function(aRow, aColumn) { },
+
+ getCellText: function PTV_getCellText(aRow, aColumn) {
+ let node = this._getNodeForRow(aRow);
+ switch (this._getColumnType(aColumn)) {
+ case this.COLUMN_TYPE_TITLE:
+ // normally, this is just the title, but we don't want empty items in
+ // the tree view so return a special string if the title is empty.
+ // Do it here so that callers can still get at the 0 length title
+ // if they go through the "result" API.
+ if (PlacesUtils.nodeIsSeparator(node))
+ return "";
+ return PlacesUIUtils.getBestTitle(node, true);
+ case this.COLUMN_TYPE_TAGS:
+ return node.tags;
+ case this.COLUMN_TYPE_URI:
+ if (PlacesUtils.nodeIsURI(node))
+ return node.uri;
+ return "";
+ case this.COLUMN_TYPE_DATE:
+ let nodeTime = node.time;
+ if (nodeTime == 0 || !PlacesUtils.nodeIsURI(node)) {
+ // hosts and days shouldn't have a value for the date column.
+ // Actually, you could argue this point, but looking at the
+ // results, seeing the most recently visited date is not what
+ // I expect, and gives me no information I know how to use.
+ // Only show this for URI-based items.
+ return "";
+ }
+
+ return this._convertPRTimeToString(nodeTime);
+ case this.COLUMN_TYPE_VISITCOUNT:
+ return node.accessCount;
+ case this.COLUMN_TYPE_DESCRIPTION:
+ if (node.itemId != -1) {
+ try {
+ return PlacesUtils.annotations.
+ getItemAnnotation(node.itemId, PlacesUIUtils.DESCRIPTION_ANNO);
+ }
+ catch (ex) { /* has no description */ }
+ }
+ return "";
+ case this.COLUMN_TYPE_DATEADDED:
+ if (node.dateAdded)
+ return this._convertPRTimeToString(node.dateAdded);
+ return "";
+ case this.COLUMN_TYPE_LASTMODIFIED:
+ if (node.lastModified)
+ return this._convertPRTimeToString(node.lastModified);
+ return "";
+ }
+ return "";
+ },
+
+ setTree: function PTV_setTree(aTree) {
+ // If we are replacing the tree during a batch, there is a concrete risk
+ // that the treeView goes out of sync, thus it's safer to end the batch now.
+ // This is a no-op if we are not batching.
+ this.batching(false);
+
+ let hasOldTree = this._tree != null;
+ this._tree = aTree;
+
+ if (this._result) {
+ if (hasOldTree) {
+ // detach from result when we are detaching from the tree.
+ // This breaks the reference cycle between us and the result.
+ if (!aTree) {
+ this._result.removeObserver(this);
+ this._rootNode.containerOpen = false;
+ }
+ }
+ if (aTree)
+ this._finishInit();
+ }
+ },
+
+ toggleOpenState: function PTV_toggleOpenState(aRow) {
+ if (!this._result)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ let node = this._rows[aRow];
+ if (this._flatList && this._openContainerCallback) {
+ this._openContainerCallback(node);
+ return;
+ }
+
+ // Persist containers open status, but never persist livemarks.
+ if (!this._controller.hasCachedLivemarkInfo(node)) {
+ let uri = node.uri;
+
+ if (uri) {
+ let docURI = document.documentURI;
+
+ if (node.containerOpen) {
+ this._xulStore.removeValue(docURI, uri, "open");
+ } else {
+ this._xulStore.setValue(docURI, uri, "open", "true");
+ }
+ }
+ }
+
+ node.containerOpen = !node.containerOpen;
+ },
+
+ cycleHeader: function PTV_cycleHeader(aColumn) {
+ if (!this._result)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ // Sometimes you want a tri-state sorting, and sometimes you don't. This
+ // rule allows tri-state sorting when the root node is a folder. This will
+ // catch the most common cases. When you are looking at folders, you want
+ // the third state to reset the sorting to the natural bookmark order. When
+ // you are looking at history, that third state has no meaning so we try
+ // to disallow it.
+ //
+ // The problem occurs when you have a query that results in bookmark
+ // folders. One example of this is the subscriptions view. In these cases,
+ // this rule doesn't allow you to sort those sub-folders by their natural
+ // order.
+ let allowTriState = PlacesUtils.nodeIsFolder(this._result.root);
+
+ let oldSort = this._result.sortingMode;
+ let oldSortingAnnotation = this._result.sortingAnnotation;
+ let newSort;
+ let newSortingAnnotation = "";
+ const NHQO = Ci.nsINavHistoryQueryOptions;
+ switch (this._getColumnType(aColumn)) {
+ case this.COLUMN_TYPE_TITLE:
+ if (oldSort == NHQO.SORT_BY_TITLE_ASCENDING)
+ newSort = NHQO.SORT_BY_TITLE_DESCENDING;
+ else if (allowTriState && oldSort == NHQO.SORT_BY_TITLE_DESCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_TITLE_ASCENDING;
+
+ break;
+ case this.COLUMN_TYPE_URI:
+ if (oldSort == NHQO.SORT_BY_URI_ASCENDING)
+ newSort = NHQO.SORT_BY_URI_DESCENDING;
+ else if (allowTriState && oldSort == NHQO.SORT_BY_URI_DESCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_URI_ASCENDING;
+
+ break;
+ case this.COLUMN_TYPE_DATE:
+ if (oldSort == NHQO.SORT_BY_DATE_ASCENDING)
+ newSort = NHQO.SORT_BY_DATE_DESCENDING;
+ else if (allowTriState &&
+ oldSort == NHQO.SORT_BY_DATE_DESCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_DATE_ASCENDING;
+
+ break;
+ case this.COLUMN_TYPE_VISITCOUNT:
+ // visit count default is unusual because we sort by descending
+ // by default because you are most likely to be looking for
+ // highly visited sites when you click it
+ if (oldSort == NHQO.SORT_BY_VISITCOUNT_DESCENDING)
+ newSort = NHQO.SORT_BY_VISITCOUNT_ASCENDING;
+ else if (allowTriState && oldSort == NHQO.SORT_BY_VISITCOUNT_ASCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_VISITCOUNT_DESCENDING;
+
+ break;
+ case this.COLUMN_TYPE_DESCRIPTION:
+ if (oldSort == NHQO.SORT_BY_ANNOTATION_ASCENDING &&
+ oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) {
+ newSort = NHQO.SORT_BY_ANNOTATION_DESCENDING;
+ newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO;
+ }
+ else if (allowTriState &&
+ oldSort == NHQO.SORT_BY_ANNOTATION_DESCENDING &&
+ oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
+ newSort = NHQO.SORT_BY_NONE;
+ else {
+ newSort = NHQO.SORT_BY_ANNOTATION_ASCENDING;
+ newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO;
+ }
+
+ break;
+ case this.COLUMN_TYPE_DATEADDED:
+ if (oldSort == NHQO.SORT_BY_DATEADDED_ASCENDING)
+ newSort = NHQO.SORT_BY_DATEADDED_DESCENDING;
+ else if (allowTriState &&
+ oldSort == NHQO.SORT_BY_DATEADDED_DESCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_DATEADDED_ASCENDING;
+
+ break;
+ case this.COLUMN_TYPE_LASTMODIFIED:
+ if (oldSort == NHQO.SORT_BY_LASTMODIFIED_ASCENDING)
+ newSort = NHQO.SORT_BY_LASTMODIFIED_DESCENDING;
+ else if (allowTriState &&
+ oldSort == NHQO.SORT_BY_LASTMODIFIED_DESCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_LASTMODIFIED_ASCENDING;
+
+ break;
+ case this.COLUMN_TYPE_TAGS:
+ if (oldSort == NHQO.SORT_BY_TAGS_ASCENDING)
+ newSort = NHQO.SORT_BY_TAGS_DESCENDING;
+ else if (allowTriState && oldSort == NHQO.SORT_BY_TAGS_DESCENDING)
+ newSort = NHQO.SORT_BY_NONE;
+ else
+ newSort = NHQO.SORT_BY_TAGS_ASCENDING;
+
+ break;
+ default:
+ throw Cr.NS_ERROR_INVALID_ARG;
+ }
+ this._result.sortingAnnotation = newSortingAnnotation;
+ this._result.sortingMode = newSort;
+ },
+
+ isEditable: function PTV_isEditable(aRow, aColumn) {
+ // At this point we only support editing the title field.
+ if (aColumn.index != 0)
+ return false;
+
+ let node = this._rows[aRow];
+ if (!node) {
+ Cu.reportError("isEditable called for an unbuilt row.");
+ return false;
+ }
+ let itemId = node.itemId;
+
+ // Only bookmark-nodes are editable. Fortunately, this checks also takes
+ // care of livemark children.
+ if (itemId == -1)
+ return false;
+
+ // The following items are also not editable, even though they are bookmark
+ // items.
+ // * places-roots
+ // * the left pane special folders and queries (those are place: uri
+ // bookmarks)
+ // * separators
+ //
+ // Note that concrete itemIds aren't used intentionally. For example, we
+ // have no reason to disallow renaming a shortcut to the Bookmarks Toolbar,
+ // except for the one under All Bookmarks.
+ if (PlacesUtils.nodeIsSeparator(node) || PlacesUtils.isRootItem(itemId))
+ return false;
+
+ let parentId = PlacesUtils.getConcreteItemId(node.parent);
+ if (parentId == PlacesUIUtils.leftPaneFolderId ||
+ parentId == PlacesUIUtils.allBookmarksFolderId) {
+ // Note that the for the time being this is the check that actually
+ // blocks renaming places "roots", and not the isRootItem check above.
+ // That's because places root are only exposed through folder shortcuts
+ // descendants of the left pane folder.
+ return false;
+ }
+
+ return true;
+ },
+
+ setCellText: function PTV_setCellText(aRow, aColumn, aText) {
+ // We may only get here if the cell is editable.
+ let node = this._rows[aRow];
+ if (node.title != aText) {
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let txn = new PlacesEditItemTitleTransaction(node.itemId, aText);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ return;
+ }
+ PlacesTransactions.EditTitle({ guid: node.bookmarkGuid, title: aText })
+ .transact().catch(Cu.reportError);
+ }
+ },
+
+ toggleCutNode: function PTV_toggleCutNode(aNode, aValue) {
+ let currentVal = this._cuttingNodes.has(aNode);
+ if (currentVal != aValue) {
+ if (aValue)
+ this._cuttingNodes.add(aNode);
+ else
+ this._cuttingNodes.delete(aNode);
+
+ this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
+ }
+ },
+
+ selectionChanged: function() { },
+ cycleCell: function(aRow, aColumn) { },
+ isSelectable: function(aRow, aColumn) { return false; },
+ performAction: function(aAction) { },
+ performActionOnRow: function(aAction, aRow) { },
+ performActionOnCell: function(aAction, aRow, aColumn) { }
+};
diff --git a/browser/components/places/jar.mn b/browser/components/places/jar.mn
new file mode 100644
index 000000000..93809510d
--- /dev/null
+++ b/browser/components/places/jar.mn
@@ -0,0 +1,34 @@
+# 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/.
+
+browser.jar:
+% overlay chrome://browser/content/places/places.xul chrome://browser/content/places/downloadsViewOverlay.xul
+# Provide another URI for the bookmarkProperties dialog so we can persist the
+# attributes separately
+ content/browser/places/bookmarkProperties2.xul (content/bookmarkProperties.xul)
+* content/browser/places/places.xul (content/places.xul)
+ content/browser/places/places.js (content/places.js)
+ content/browser/places/places.css (content/places.css)
+ content/browser/places/organizer.css (content/organizer.css)
+ content/browser/places/bookmarkProperties.xul (content/bookmarkProperties.xul)
+ content/browser/places/bookmarkProperties.js (content/bookmarkProperties.js)
+ content/browser/places/placesOverlay.xul (content/placesOverlay.xul)
+ content/browser/places/menu.xml (content/menu.xml)
+ content/browser/places/tree.xml (content/tree.xml)
+ content/browser/places/controller.js (content/controller.js)
+ content/browser/places/treeView.js (content/treeView.js)
+ content/browser/places/browserPlacesViews.js (content/browserPlacesViews.js)
+# keep the Places version of the history sidebar at history/history-panel.xul
+# to prevent having to worry about between versions of the browser
+* content/browser/history/history-panel.xul (content/history-panel.xul)
+ content/browser/places/history-panel.js (content/history-panel.js)
+# ditto for the bookmarks sidebar
+ content/browser/bookmarks/bookmarksPanel.xul (content/bookmarksPanel.xul)
+ content/browser/bookmarks/bookmarksPanel.js (content/bookmarksPanel.js)
+ content/browser/bookmarks/sidebarUtils.js (content/sidebarUtils.js)
+ content/browser/places/moveBookmarks.xul (content/moveBookmarks.xul)
+ content/browser/places/moveBookmarks.js (content/moveBookmarks.js)
+ content/browser/places/editBookmarkOverlay.xul (content/editBookmarkOverlay.xul)
+ content/browser/places/editBookmarkOverlay.js (content/editBookmarkOverlay.js)
+* content/browser/places/downloadsViewOverlay.xul (content/downloadsViewOverlay.xul)
diff --git a/browser/components/places/moz.build b/browser/components/places/moz.build
new file mode 100644
index 000000000..e6f88b318
--- /dev/null
+++ b/browser/components/places/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
+MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
+
+JAR_MANIFESTS += ['jar.mn']
+
+EXTRA_JS_MODULES += [
+ 'PlacesUIUtils.jsm',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Firefox', 'Bookmarks & History')
diff --git a/browser/components/places/tests/browser/.eslintrc.js b/browser/components/places/tests/browser/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/browser/components/places/tests/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/components/places/tests/browser/bookmark_dummy_1.html b/browser/components/places/tests/browser/bookmark_dummy_1.html
new file mode 100644
index 000000000..c03e0c18c
--- /dev/null
+++ b/browser/components/places/tests/browser/bookmark_dummy_1.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Bookmark Dummy 1</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Bookmark Dummy 1</p>
+</body>
+</html>
diff --git a/browser/components/places/tests/browser/bookmark_dummy_2.html b/browser/components/places/tests/browser/bookmark_dummy_2.html
new file mode 100644
index 000000000..229a730b3
--- /dev/null
+++ b/browser/components/places/tests/browser/bookmark_dummy_2.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Bookmark Dummy 2</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Bookmark Dummy 2</p>
+</body>
+</html>
diff --git a/browser/components/places/tests/browser/browser.ini b/browser/components/places/tests/browser/browser.ini
new file mode 100644
index 000000000..5dce31653
--- /dev/null
+++ b/browser/components/places/tests/browser/browser.ini
@@ -0,0 +1,58 @@
+# 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/.
+
+[DEFAULT]
+support-files =
+ head.js
+ framedPage.html
+ frameLeft.html
+ frameRight.html
+ sidebarpanels_click_test_page.html
+ keyword_form.html
+
+[browser_0_library_left_pane_migration.js]
+[browser_410196_paste_into_tags.js]
+subsuite = clipboard
+[browser_416459_cut.js]
+subsuite = clipboard
+[browser_423515.js]
+[browser_425884.js]
+[browser_435851_copy_query.js]
+subsuite = clipboard
+[browser_475045.js]
+[browser_555547.js]
+[browser_bookmarklet_windowOpen.js]
+support-files =
+ pageopeningwindow.html
+[browser_bookmarkProperties_addFolderDefaultButton.js]
+[browser_bookmarkProperties_addKeywordForThisSearch.js]
+[browser_bookmarkProperties_addLivemark.js]
+[browser_bookmarkProperties_editTagContainer.js]
+[browser_bookmarkProperties_readOnlyRoot.js]
+[browser_bookmarksProperties.js]
+[browser_drag_bookmarks_on_toolbar.js]
+[browser_forgetthissite_single.js]
+[browser_history_sidebar_search.js]
+[browser_library_batch_delete.js]
+[browser_library_commands.js]
+[browser_library_downloads.js]
+[browser_library_infoBox.js]
+[browser_library_left_pane_fixnames.js]
+[browser_library_left_pane_select_hierarchy.js]
+[browser_library_middleclick.js]
+[browser_library_open_leak.js]
+[browser_library_openFlatContainer.js]
+[browser_library_panel_leak.js]
+[browser_library_search.js]
+[browser_library_views_liveupdate.js]
+[browser_markPageAsFollowedLink.js]
+[browser_sidebarpanels_click.js]
+skip-if = true # temporarily disabled for breaking the treeview - bug 658744
+[browser_sort_in_library.js]
+[browser_toolbarbutton_menu_context.js]
+[browser_views_liveupdate.js]
+[browser_bookmark_all_tabs.js]
+support-files =
+ bookmark_dummy_1.html
+ bookmark_dummy_2.html
diff --git a/browser/components/places/tests/browser/browser_0_library_left_pane_migration.js b/browser/components/places/tests/browser/browser_0_library_left_pane_migration.js
new file mode 100644
index 000000000..a7089b497
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_0_library_left_pane_migration.js
@@ -0,0 +1,90 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+"use strict";
+
+/**
+ * Test we correctly migrate Library left pane to the latest version.
+ * Note: this test MUST be the first between browser chrome tests, or results
+ * of next tests could be unexpected due to PlacesUIUtils getters.
+ */
+
+const TEST_URI = "http://www.mozilla.org/";
+
+add_task(function* () {
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils is running in chrome context");
+ ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+ ok(PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION > 0,
+ "Left pane version in chrome context, current version is: " + PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION );
+
+ // Check if we have any left pane folder already set, remove it eventually.
+ let leftPaneItems = PlacesUtils.annotations
+ .getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ if (leftPaneItems.length > 0) {
+ // The left pane has already been created, touching it now would cause
+ // next tests to rely on wrong values (and possibly crash)
+ is(leftPaneItems.length, 1, "We correctly have only 1 left pane folder");
+ // Check version.
+ let version = PlacesUtils.annotations.getItemAnnotation(leftPaneItems[0],
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(version, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, "Left pane version is actual");
+ ok(true, "left pane has already been created, skipping test");
+ return;
+ }
+
+ // Create a fake left pane folder with an old version (current version - 1).
+ let folder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.rootGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: ""
+ });
+
+ let folderId = yield PlacesUtils.promiseItemId(folder.guid);
+ PlacesUtils.annotations.setItemAnnotation(folderId,
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO,
+ PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION - 1,
+ 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ // Check fake left pane root has been correctly created.
+ leftPaneItems =
+ PlacesUtils.annotations.getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(leftPaneItems.length, 1, "We correctly have only 1 left pane folder");
+ is(leftPaneItems[0], folderId, "left pane root itemId is correct");
+
+ // Check version.
+ let version = PlacesUtils.annotations.getItemAnnotation(folderId,
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(version, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION - 1, "Left pane version correctly set");
+
+ // Open Library, this will upgrade our left pane version.
+ let organizer = yield promiseLibrary();
+
+ // Check left pane.
+ ok(PlacesUIUtils.leftPaneFolderId > 0, "Left pane folder correctly created");
+ leftPaneItems =
+ PlacesUtils.annotations.getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(leftPaneItems.length, 1, "We correctly have only 1 left pane folder");
+ let leftPaneRoot = leftPaneItems[0];
+ is(leftPaneRoot, PlacesUIUtils.leftPaneFolderId,
+ "leftPaneFolderId getter has correct value");
+
+ // Check version has been upgraded.
+ version = PlacesUtils.annotations.getItemAnnotation(leftPaneRoot,
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(version, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION,
+ "Left pane version has been correctly upgraded");
+
+ // Check left pane is populated.
+ organizer.PlacesOrganizer.selectLeftPaneQuery("History");
+ is(organizer.PlacesOrganizer._places.selectedNode.itemId,
+ PlacesUIUtils.leftPaneQueries["History"],
+ "Library left pane is populated and working");
+
+ yield promiseLibraryClosed(organizer);
+});
diff --git a/browser/components/places/tests/browser/browser_410196_paste_into_tags.js b/browser/components/places/tests/browser/browser_410196_paste_into_tags.js
new file mode 100644
index 000000000..2acb1d9b7
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_410196_paste_into_tags.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const TEST_URL = Services.io.newURI("http://example.com/", null, null);
+const MOZURISPEC = Services.io.newURI("http://mozilla.com/", null, null);
+
+add_task(function* () {
+ let organizer = yield promiseLibrary();
+
+ ok(PlacesUtils, "PlacesUtils in scope");
+ ok(PlacesUIUtils, "PlacesUIUtils in scope");
+
+ let PlacesOrganizer = organizer.PlacesOrganizer;
+ ok(PlacesOrganizer, "Places organizer in scope");
+
+ let ContentTree = organizer.ContentTree;
+ ok(ContentTree, "ContentTree is in scope");
+
+ let visits = {uri: MOZURISPEC, transition: PlacesUtils.history.TRANSITION_TYPED};
+ yield PlacesTestUtils.addVisits(visits);
+
+ // create an initial tag to work with
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ title: "bookmark/" + TEST_URL.spec,
+ url: TEST_URL
+ });
+
+ ok(bm, "A bookmark was added");
+ PlacesUtils.tagging.tagURI(TEST_URL, ["foo"]);
+ let tags = PlacesUtils.tagging.getTagsForURI(TEST_URL);
+ is(tags[0], "foo", "tag is foo");
+
+ // focus the new tag
+ focusTag(PlacesOrganizer);
+
+ let populate = () => copyHistNode(PlacesOrganizer, ContentTree);
+ yield promiseClipboard(populate, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ focusTag(PlacesOrganizer);
+ PlacesOrganizer._places.controller.paste();
+
+ // re-focus the history again
+ PlacesOrganizer.selectLeftPaneQuery("History");
+ let histContainer = PlacesOrganizer._places.selectedNode;
+ PlacesUtils.asContainer(histContainer);
+ histContainer.containerOpen = true;
+ PlacesOrganizer._places.selectNode(histContainer.getChild(0));
+ let histNode = ContentTree.view.view.nodeForTreeIndex(0);
+ ok(histNode, "histNode exists: " + histNode.title);
+
+ // check to see if the history node is tagged!
+ tags = PlacesUtils.tagging.getTagsForURI(MOZURISPEC);
+ ok(tags.length == 1, "history node is tagged: " + tags.length);
+
+ // check if a bookmark was created
+ let bookmarks = [];
+ yield PlacesUtils.bookmarks.fetch({url: MOZURISPEC}, bm => {
+ bookmarks.push(bm);
+ });
+ ok(bookmarks.length > 0, "bookmark exists for the tagged history item");
+
+ // is the bookmark visible in the UI?
+ // get the Unsorted Bookmarks node
+ PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+
+ // now we can see what is in the ContentTree tree
+ let unsortedNode = ContentTree.view.view.nodeForTreeIndex(1);
+ ok(unsortedNode, "unsortedNode is not null: " + unsortedNode.uri);
+ is(unsortedNode.uri, MOZURISPEC.spec, "node uri's are the same");
+
+ yield promiseLibraryClosed(organizer);
+
+ // Remove new Places data we created.
+ PlacesUtils.tagging.untagURI(MOZURISPEC, ["foo"]);
+ PlacesUtils.tagging.untagURI(TEST_URL, ["foo"]);
+ tags = PlacesUtils.tagging.getTagsForURI(TEST_URL);
+ is(tags.length, 0, "tags are gone");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+function focusTag(PlacesOrganizer) {
+ PlacesOrganizer.selectLeftPaneQuery("Tags");
+ let tags = PlacesOrganizer._places.selectedNode;
+ tags.containerOpen = true;
+ let fooTag = tags.getChild(0);
+ let tagNode = fooTag;
+ PlacesOrganizer._places.selectNode(fooTag);
+ is(tagNode.title, 'foo', "tagNode title is foo");
+ let ip = PlacesOrganizer._places.insertionPoint;
+ ok(ip.isTag, "IP is a tag");
+}
+
+function copyHistNode(PlacesOrganizer, ContentTree) {
+ // focus the history object
+ PlacesOrganizer.selectLeftPaneQuery("History");
+ let histContainer = PlacesOrganizer._places.selectedNode;
+ PlacesUtils.asContainer(histContainer);
+ histContainer.containerOpen = true;
+ PlacesOrganizer._places.selectNode(histContainer.getChild(0));
+ let histNode = ContentTree.view.view.nodeForTreeIndex(0);
+ ContentTree.view.selectNode(histNode);
+ is(histNode.uri, MOZURISPEC.spec,
+ "historyNode exists: " + histNode.uri);
+ // copy the history node
+ ContentTree.view.controller.copy();
+}
diff --git a/browser/components/places/tests/browser/browser_416459_cut.js b/browser/components/places/tests/browser/browser_416459_cut.js
new file mode 100644
index 000000000..6f3f8cdd5
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_416459_cut.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "http://example.com/";
+
+add_task(function* () {
+ yield PlacesUtils.bookmarks.eraseEverything();
+ let organizer = yield promiseLibrary();
+
+ registerCleanupFunction(function* () {
+ yield promiseLibraryClosed(organizer);
+ yield PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ let PlacesOrganizer = organizer.PlacesOrganizer;
+ let ContentTree = organizer.ContentTree;
+
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils in scope");
+ ok(PlacesUIUtils, "PlacesUIUtils in scope");
+ ok(PlacesOrganizer, "PlacesOrganizer in scope");
+ ok(ContentTree, "ContentTree is in scope");
+
+ // Test with multiple entries to ensure they retain their order.
+ let bookmarks = [];
+ bookmarks.push(yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: TEST_URL,
+ title: "0"
+ }));
+ bookmarks.push(yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: TEST_URL,
+ title: "1"
+ }));
+ bookmarks.push(yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: TEST_URL,
+ title: "2"
+ }));
+
+ yield selectBookmarksIn(organizer, bookmarks, "BookmarksToolbar");
+
+ yield promiseClipboard(() => {
+ info("Cutting selection");
+ ContentTree.view.controller.cut();
+ }, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ info("Selecting UnfiledBookmarks in the left pane");
+ PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ info("Pasting clipboard");
+ ContentTree.view.controller.paste();
+
+ yield selectBookmarksIn(organizer, bookmarks, "UnfiledBookmarks");
+});
+
+var selectBookmarksIn = Task.async(function* (organizer, bookmarks, aLeftPaneQuery) {
+ let PlacesOrganizer = organizer.PlacesOrganizer;
+ let ContentTree = organizer.ContentTree;
+ info("Selecting " + aLeftPaneQuery + " in the left pane");
+ PlacesOrganizer.selectLeftPaneQuery(aLeftPaneQuery);
+
+ let ids = [];
+ for (let {guid} of bookmarks) {
+ let bookmark = yield PlacesUtils.bookmarks.fetch(guid);
+ is (bookmark.parentGuid, PlacesOrganizer._places.selectedNode.targetFolderGuid,
+ "Bookmark has the right parent");
+ ids.push(yield PlacesUtils.promiseItemId(bookmark.guid));
+ }
+
+ info("Selecting the bookmarks in the right pane");
+ ContentTree.view.selectItems(ids);
+
+ for (let node of ContentTree.view.selectedNodes) {
+ is(node.bookmarkIndex, node.title,
+ "Found the expected bookmark in the expected position");
+ }
+});
diff --git a/browser/components/places/tests/browser/browser_423515.js b/browser/components/places/tests/browser/browser_423515.js
new file mode 100644
index 000000000..4d3da8fc1
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_423515.js
@@ -0,0 +1,173 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+function test() {
+ // sanity check
+ ok(PlacesUtils, "checking PlacesUtils, running in chrome context?");
+ ok(PlacesUIUtils, "checking PlacesUIUtils, running in chrome context?");
+ ok(PlacesControllerDragHelper, "checking PlacesControllerDragHelper, running in chrome context?");
+
+ const IDX = PlacesUtils.bookmarks.DEFAULT_INDEX;
+
+ // setup
+ var rootId = PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId, "", IDX);
+ var rootNode = PlacesUtils.getFolderContents(rootId, false, true).root;
+ is(rootNode.childCount, 0, "confirm test root is empty");
+
+ var tests = [];
+
+ // add a regular folder, should be moveable
+ tests.push({
+ populate: function() {
+ this.id =
+ PlacesUtils.bookmarks.createFolder(rootId, "", IDX);
+ },
+ validate: function() {
+ is(rootNode.childCount, 1,
+ "populate added data to the test root");
+ is(PlacesControllerDragHelper.canMoveNode(rootNode.getChild(0)),
+ true, "can move regular folder node");
+ }
+ });
+
+ // add a regular folder shortcut, should be moveable
+ tests.push({
+ populate: function() {
+ this.folderId =
+ PlacesUtils.bookmarks.createFolder(rootId, "foo", IDX);
+ this.shortcutId =
+ PlacesUtils.bookmarks.insertBookmark(rootId, makeURI("place:folder="+this.folderId), IDX, "bar");
+ },
+ validate: function() {
+ is(rootNode.childCount, 2,
+ "populated data to the test root");
+
+ var folderNode = rootNode.getChild(0);
+ is(folderNode.type, 6, "node is folder");
+ is(this.folderId, folderNode.itemId, "folder id and folder node item id match");
+
+ var shortcutNode = rootNode.getChild(1);
+ is(shortcutNode.type, 9, "node is folder shortcut");
+ is(this.shortcutId, shortcutNode.itemId, "shortcut id and shortcut node item id match");
+
+ var concreteId = PlacesUtils.getConcreteItemId(shortcutNode);
+ is(concreteId, folderNode.itemId, "shortcut node id and concrete id match");
+
+ is(PlacesControllerDragHelper.canMoveNode(shortcutNode),
+ true, "can move folder shortcut node");
+ }
+ });
+
+ // add a regular query, should be moveable
+ tests.push({
+ populate: function() {
+ this.bookmarkId =
+ PlacesUtils.bookmarks.insertBookmark(rootId, makeURI("http://foo.com"), IDX, "foo");
+ this.queryId =
+ PlacesUtils.bookmarks.insertBookmark(rootId, makeURI("place:terms=foo"), IDX, "bar");
+ },
+ validate: function() {
+ is(rootNode.childCount, 2,
+ "populated data to the test root");
+
+ var bmNode = rootNode.getChild(0);
+ is(bmNode.itemId, this.bookmarkId, "bookmark id and bookmark node item id match");
+
+ var queryNode = rootNode.getChild(1);
+ is(queryNode.itemId, this.queryId, "query id and query node item id match");
+
+ is(PlacesControllerDragHelper.canMoveNode(queryNode),
+ true, "can move query node");
+ }
+ });
+
+ // test that special folders cannot be moved
+ // test that special folders shortcuts can be moved
+ tests.push({
+ folders: [PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.tagsFolderId, PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.toolbarFolderId],
+ shortcuts: {},
+ populate: function() {
+ for (var i = 0; i < this.folders.length; i++) {
+ var id = this.folders[i];
+ this.shortcuts[id] =
+ PlacesUtils.bookmarks.insertBookmark(rootId, makeURI("place:folder=" + id), IDX, "");
+ }
+ },
+ validate: function() {
+ // test toolbar shortcut node
+ is(rootNode.childCount, this.folders.length,
+ "populated data to the test root");
+
+ function getRootChildNode(aId) {
+ var node = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, false, true).root;
+ for (var i = 0; i < node.childCount; i++) {
+ var child = node.getChild(i);
+ if (child.itemId == aId) {
+ node.containerOpen = false;
+ return child;
+ }
+ }
+ node.containerOpen = false;
+ ok(false, "Unable to find child node");
+ return null;
+ }
+
+ for (var i = 0; i < this.folders.length; i++) {
+ var id = this.folders[i];
+
+ var node = getRootChildNode(id);
+ isnot(node, null, "Node found");
+ is(PlacesControllerDragHelper.canMoveNode(node),
+ false, "shouldn't be able to move special folder node");
+
+ var shortcutId = this.shortcuts[id];
+ var shortcutNode = rootNode.getChild(i);
+
+ is(shortcutNode.itemId, shortcutId, "shortcut id and shortcut node item id match");
+
+ dump("can move shortcut node?\n");
+ is(PlacesControllerDragHelper.canMoveNode(shortcutNode),
+ true, "should be able to move special folder shortcut node");
+ }
+ }
+ });
+
+ // test that a tag container cannot be moved
+ tests.push({
+ populate: function() {
+ // tag a uri
+ this.uri = makeURI("http://foo.com");
+ PlacesUtils.tagging.tagURI(this.uri, ["bar"]);
+ registerCleanupFunction(() => PlacesUtils.tagging.untagURI(this.uri, ["bar"]));
+ },
+ validate: function() {
+ // get tag root
+ var query = PlacesUtils.history.getNewQuery();
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY;
+ var tagsNode = PlacesUtils.history.executeQuery(query, options).root;
+
+ tagsNode.containerOpen = true;
+ is(tagsNode.childCount, 1, "has new tag");
+
+ var tagNode = tagsNode.getChild(0);
+
+ is(PlacesControllerDragHelper.canMoveNode(tagNode),
+ false, "should not be able to move tag container node");
+ tagsNode.containerOpen = false;
+ }
+ });
+
+ tests.forEach(function(aTest) {
+ PlacesUtils.bookmarks.removeFolderChildren(rootId);
+ aTest.populate();
+ aTest.validate();
+ });
+
+ rootNode.containerOpen = false;
+ PlacesUtils.bookmarks.removeItem(rootId);
+}
diff --git a/browser/components/places/tests/browser/browser_425884.js b/browser/components/places/tests/browser/browser_425884.js
new file mode 100644
index 000000000..655eb1ffd
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_425884.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ Deep copy of bookmark data, using the front-end codepath:
+
+ - create test folder A
+ - add a subfolder to folder A, and add items to it
+ - validate folder A (sanity check)
+ - copy folder A, creating new folder B, using the front-end path
+ - validate folder B
+ - undo copy transaction
+ - validate folder B (empty)
+ - redo copy transaction
+ - validate folder B's contents
+*/
+
+add_task(function* () {
+ // sanity check
+ ok(PlacesUtils, "checking PlacesUtils, running in chrome context?");
+ ok(PlacesUIUtils, "checking PlacesUIUtils, running in chrome context?");
+
+ let toolbarId = PlacesUtils.toolbarFolderId;
+ let toolbarNode = PlacesUtils.getFolderContents(toolbarId).root;
+
+ let oldCount = toolbarNode.childCount;
+ let testRoot = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "test root"
+ });
+ is(toolbarNode.childCount, oldCount+1, "confirm test root node is a container, and is empty");
+
+ let testRootNode = toolbarNode.getChild(toolbarNode.childCount-1);
+ testRootNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ testRootNode.containerOpen = true;
+ is(testRootNode.childCount, 0, "confirm test root node is a container, and is empty");
+
+ // create folder A, fill it, validate its contents
+ let folderA = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: testRoot.guid,
+ title: "A"
+ });
+
+ yield populate(folderA);
+
+ let folderAId = yield PlacesUtils.promiseItemId(folderA.guid);
+ let folderANode = PlacesUtils.getFolderContents(folderAId).root;
+ validate(folderANode);
+ is(testRootNode.childCount, 1, "create test folder");
+
+ // copy it, using the front-end helper functions
+ let serializedNode = PlacesUtils.wrapNode(folderANode, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
+ let rawNode = PlacesUtils.unwrapNodes(serializedNode, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER).shift();
+ // confirm serialization
+ ok(rawNode.type, "confirm json node");
+ folderANode.containerOpen = false;
+
+ let testRootId = yield PlacesUtils.promiseItemId(testRoot.guid);
+ let transaction = PlacesUIUtils.makeTransaction(rawNode,
+ PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
+ testRootId,
+ -1,
+ true);
+ ok(transaction, "create transaction");
+ PlacesUtils.transactionManager.doTransaction(transaction);
+ // confirm copy
+ is(testRootNode.childCount, 2, "create test folder via copy");
+
+ // validate the copy
+ let folderBNode = testRootNode.getChild(1);
+ validate(folderBNode);
+
+ // undo the transaction, confirm the removal
+ PlacesUtils.transactionManager.undoTransaction();
+ is(testRootNode.childCount, 1, "confirm undo removed the copied folder");
+
+ // redo the transaction
+ PlacesUtils.transactionManager.redoTransaction();
+ is(testRootNode.childCount, 2, "confirm redo re-copied the folder");
+ folderBNode = testRootNode.getChild(1);
+ validate(folderBNode);
+
+ // Close containers, cleaning up their observers.
+ testRootNode.containerOpen = false;
+ toolbarNode.containerOpen = false;
+
+ // clean up
+ PlacesUtils.transactionManager.undoTransaction();
+ yield PlacesUtils.bookmarks.remove(folderA.guid);
+});
+
+var populate = Task.async(function* (parentFolder) {
+ let folder = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: parentFolder.guid,
+ title: "test folder"
+ });
+
+ yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: folder.guid,
+ title: "test bookmark",
+ url: "http://foo"
+ });
+
+ yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ parentGuid: folder.guid
+ });
+});
+
+function validate(aNode) {
+ PlacesUtils.asContainer(aNode);
+ aNode.containerOpen = true;
+ is(aNode.childCount, 1, "confirm child count match");
+ var folderNode = aNode.getChild(0);
+ is(folderNode.title, "test folder", "confirm folder title");
+ PlacesUtils.asContainer(folderNode);
+ folderNode.containerOpen = true;
+ is(folderNode.childCount, 2, "confirm child count match");
+ folderNode.containerOpen = false;
+ aNode.containerOpen = false;
+}
diff --git a/browser/components/places/tests/browser/browser_435851_copy_query.js b/browser/components/places/tests/browser/browser_435851_copy_query.js
new file mode 100644
index 000000000..92f818b41
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_435851_copy_query.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* test that copying a non movable query or folder shortcut makes a new query with the same url, not a deep copy */
+
+const SHORTCUT_URL = "place:folder=2";
+const QUERY_URL = "place:sort=8&maxResults=10";
+
+add_task(function* copy_toolbar_shortcut() {
+ let library = yield promiseLibrary();
+
+ registerCleanupFunction(function () {
+ library.close();
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ });
+
+ library.PlacesOrganizer.selectLeftPaneQuery("BookmarksToolbar");
+
+ yield promiseClipboard(function () { library.PlacesOrganizer._places.controller.copy(); },
+ PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ library.PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ library.ContentTree.view.controller.paste();
+
+ let toolbarCopyNode = library.ContentTree.view.view.nodeForTreeIndex(0);
+ is(toolbarCopyNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
+ "copy is still a folder shortcut");
+
+ PlacesUtils.bookmarks.removeItem(toolbarCopyNode.itemId);
+ library.PlacesOrganizer.selectLeftPaneQuery("BookmarksToolbar");
+ is(library.PlacesOrganizer._places.selectedNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
+ "original is still a folder shortcut");
+});
+
+add_task(function* copy_history_query() {
+ let library = yield promiseLibrary();
+
+ library.PlacesOrganizer.selectLeftPaneQuery("History");
+
+ yield promiseClipboard(function () { library.PlacesOrganizer._places.controller.copy(); },
+ PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ library.PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ library.ContentTree.view.controller.paste();
+
+ let historyCopyNode = library.ContentTree.view.view.nodeForTreeIndex(0);
+ is(historyCopyNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY,
+ "copy is still a query");
+
+ PlacesUtils.bookmarks.removeItem(historyCopyNode.itemId);
+ library.PlacesOrganizer.selectLeftPaneQuery("History");
+ is(library.PlacesOrganizer._places.selectedNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY,
+ "original is still a query");
+});
diff --git a/browser/components/places/tests/browser/browser_475045.js b/browser/components/places/tests/browser/browser_475045.js
new file mode 100644
index 000000000..7d562349b
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_475045.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+// Instead of loading EventUtils.js into the test scope in browser-test.js for all tests,
+// we only need EventUtils.js for a few files which is why we are using loadSubScript.
+var EventUtils = {};
+this._scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+add_task(function* test() {
+ // Make sure the bookmarks bar is visible and restore its state on cleanup.
+ let toolbar = document.getElementById("PersonalToolbar");
+ ok(toolbar, "PersonalToolbar should not be null");
+
+ if (toolbar.collapsed) {
+ yield promiseSetToolbarVisibility(toolbar, true);
+ registerCleanupFunction(function() {
+ return promiseSetToolbarVisibility(toolbar, false);
+ });
+ }
+
+ // Setup the node we will use to be dropped. The actual node used does not
+ // matter because we will set its data, effect, and mimeType manually.
+ let placesItems = document.getElementById("PlacesToolbarItems");
+ ok(placesItems, "PlacesToolbarItems should not be null");
+ ok(placesItems.localName == "scrollbox", "PlacesToolbarItems should not be null");
+ ok(placesItems.childNodes[0], "PlacesToolbarItems must have at least one child");
+
+ /**
+ * Simulates a drop of a URI onto the bookmarks bar.
+ *
+ * @param aEffect
+ * The effect to use for the drop operation: move, copy, or link.
+ * @param aMimeType
+ * The mime type to use for the drop operation.
+ */
+ let simulateDragDrop = function(aEffect, aMimeType) {
+ const uriSpec = "http://www.mozilla.org/D1995729-A152-4e30-8329-469B01F30AA7";
+ let uri = makeURI(uriSpec);
+ EventUtils.synthesizeDrop(placesItems.childNodes[0],
+ placesItems,
+ [[{type: aMimeType,
+ data: uriSpec}]],
+ aEffect, window);
+
+ // Verify that the drop produces exactly one bookmark.
+ let bookmarkIds = PlacesUtils.bookmarks
+ .getBookmarkIdsForURI(uri);
+ ok(bookmarkIds.length == 1, "There should be exactly one bookmark");
+
+ PlacesUtils.bookmarks.removeItem(bookmarkIds[0]);
+
+ // Verify that we removed the bookmark successfully.
+ ok(!PlacesUtils.bookmarks.isBookmarked(uri), "URI should be removed");
+ }
+
+ // Simulate a bookmark drop for all of the mime types and effects.
+ let mimeTypes = ["text/plain", "text/unicode", "text/x-moz-url"];
+ let effects = ["move", "copy", "link"];
+ effects.forEach(function (effect) {
+ mimeTypes.forEach(function (mimeType) {
+ simulateDragDrop(effect, mimeType);
+ });
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_555547.js b/browser/components/places/tests/browser/browser_555547.js
new file mode 100644
index 000000000..0654139cb
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_555547.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_task(function* test() {
+ let sidebarBox = document.getElementById("sidebar-box");
+ is(sidebarBox.hidden, true, "The sidebar should be hidden");
+
+ // Uncollapse the personal toolbar if needed.
+ let toolbar = document.getElementById("PersonalToolbar");
+ let wasCollapsed = toolbar.collapsed;
+ if (wasCollapsed) {
+ yield promiseSetToolbarVisibility(toolbar, true);
+ }
+
+ let sidebar = yield promiseLoadedSidebar("viewBookmarksSidebar");
+ registerCleanupFunction(() => {
+ SidebarUI.hide();
+ });
+
+ // Focus the tree and check if its controller is returned.
+ let tree = sidebar.contentDocument.getElementById("bookmarks-view");
+ tree.focus();
+
+ let controller = doGetPlacesControllerForCommand("placesCmd_copy");
+ let treeController = tree.controllers
+ .getControllerForCommand("placesCmd_copy");
+ ok(controller == treeController, "tree controller was returned");
+
+ // Open the context menu for a toolbar item, and check if the toolbar's
+ // controller is returned.
+ let toolbarItems = document.getElementById("PlacesToolbarItems");
+ EventUtils.synthesizeMouse(toolbarItems.childNodes[0],
+ 4, 4, { type: "contextmenu", button: 2 },
+ window);
+ controller = doGetPlacesControllerForCommand("placesCmd_copy");
+ let toolbarController = document.getElementById("PlacesToolbar")
+ .controllers
+ .getControllerForCommand("placesCmd_copy");
+ ok(controller == toolbarController, "the toolbar controller was returned");
+
+ document.getElementById("placesContext").hidePopup();
+
+ // Now that the context menu is closed, try to get the tree controller again.
+ tree.focus();
+ controller = doGetPlacesControllerForCommand("placesCmd_copy");
+ ok(controller == treeController, "tree controller was returned");
+
+ if (wasCollapsed) {
+ yield promiseSetToolbarVisibility(toolbar, false);
+ }
+});
+
+function promiseLoadedSidebar(cmd) {
+ return new Promise(resolve => {
+ let sidebar = document.getElementById("sidebar");
+ sidebar.addEventListener("load", function onLoad() {
+ sidebar.removeEventListener("load", onLoad, true);
+ resolve(sidebar);
+ }, true);
+
+ SidebarUI.show(cmd);
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js b/browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js
new file mode 100644
index 000000000..a1f091ebe
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js
@@ -0,0 +1,53 @@
+"use strict"
+
+add_task(function* () {
+ info("Bug 475529 - Add is the default button for the new folder dialog + " +
+ "Bug 1206376 - Changing properties of a new bookmark while adding it " +
+ "acts on the last bookmark in the current container");
+
+ // Add a new bookmark at index 0 in the unfiled folder.
+ let insertionIndex = 0;
+ let newBookmark = yield PlacesUtils.bookmarks.insert({
+ index: insertionIndex,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/",
+ });
+ let newBookmarkId = yield PlacesUtils.promiseItemId(newBookmark.guid);
+
+ yield withSidebarTree("bookmarks", function* (tree) {
+ // Select the new bookmark in the sidebar.
+ tree.selectItems([newBookmarkId]);
+ ok(tree.controller.isCommandEnabled("placesCmd_new:folder"),
+ "'placesCmd_new:folder' on current selected node is enabled");
+
+ // Create a new folder. Since the new bookmark is selected, and new items
+ // are inserted at the index of the currently selected item, the new folder
+ // will be inserted at index 0.
+ yield withBookmarksDialog(
+ false,
+ function openDialog() {
+ tree.controller.doCommand("placesCmd_new:folder");
+ },
+ function* test(dialogWin) {
+ let promiseTitleChangeNotification = promiseBookmarksNotification(
+ "onItemChanged", (itemId, prop, isAnno, val) => prop == "title" && val =="n");
+
+ fillBookmarkTextField("editBMPanel_namePicker", "n", dialogWin, false);
+
+ // Confirm and close the dialog.
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+ yield promiseTitleChangeNotification;
+
+ let newFolder = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: insertionIndex,
+ });
+
+ is(newFolder.title, "n", "folder name has been edited");
+ yield PlacesUtils.bookmarks.remove(newFolder);
+ yield PlacesUtils.bookmarks.remove(newBookmark);
+ }
+ );
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js b/browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js
new file mode 100644
index 000000000..5283a1610
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js
@@ -0,0 +1,110 @@
+"use strict"
+
+const TEST_URL = "http://mochi.test:8888/browser/browser/components/places/tests/browser/keyword_form.html";
+
+add_task(function* () {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_URL,
+ }, function* (browser) {
+ // We must wait for the context menu code to build metadata.
+ yield openContextMenuForContentSelector(browser, '#form1 > input[name="search"]');
+
+ yield withBookmarksDialog(true, AddKeywordForSearchField, function* (dialogWin) {
+ let acceptBtn = dialogWin.document.documentElement.getButton("accept");
+ ok(acceptBtn.disabled, "Accept button is disabled");
+
+ let promiseKeywordNotification = promiseBookmarksNotification(
+ "onItemChanged", (itemId, prop, isAnno, val) => prop == "keyword" && val =="kw");
+
+ fillBookmarkTextField("editBMPanel_keywordField", "kw", dialogWin);
+
+ ok(!acceptBtn.disabled, "Accept button is enabled");
+
+ // The dialog is instant apply.
+ yield promiseKeywordNotification;
+
+ // After the notification, the keywords cache will update asynchronously.
+ info("Check the keyword entry has been created");
+ let entry;
+ yield waitForCondition(function* () {
+ entry = yield PlacesUtils.keywords.fetch("kw");
+ return !!entry;
+ }, "Unable to find the expected keyword");
+ is(entry.keyword, "kw", "keyword is correct");
+ is(entry.url.href, TEST_URL, "URL is correct");
+ is(entry.postData, "accenti%3D%E0%E8%EC%F2%F9&search%3D%25s", "POST data is correct");
+
+ info("Check the charset has been saved");
+ let charset = yield PlacesUtils.getCharsetForURI(NetUtil.newURI(TEST_URL));
+ is(charset, "windows-1252", "charset is correct");
+
+ // Now check getShortcutOrURI.
+ let data = yield getShortcutOrURIAndPostData("kw test");
+ is(getPostDataString(data.postData), "accenti=\u00E0\u00E8\u00EC\u00F2\u00F9&search=test", "getShortcutOrURI POST data is correct");
+ is(data.url, TEST_URL, "getShortcutOrURI URL is correct");
+ });
+ });
+});
+
+add_task(function* reopen_same_field() {
+ yield PlacesUtils.keywords.insert({
+ url: TEST_URL,
+ keyword: "kw",
+ postData: "accenti%3D%E0%E8%EC%F2%F9&search%3D%25s"
+ });
+ registerCleanupFunction(function* () {
+ yield PlacesUtils.keywords.remove("kw");
+ });
+ // Reopening on the same input field should show the existing keyword.
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_URL,
+ }, function* (browser) {
+ // We must wait for the context menu code to build metadata.
+ yield openContextMenuForContentSelector(browser, '#form1 > input[name="search"]');
+
+ yield withBookmarksDialog(true, AddKeywordForSearchField, function* (dialogWin) {
+ let acceptBtn = dialogWin.document.documentElement.getButton("accept");
+ ok(acceptBtn.disabled, "Accept button is disabled");
+
+ let elt = dialogWin.document.getElementById("editBMPanel_keywordField");
+ is(elt.value, "kw");
+ });
+ });
+});
+
+add_task(function* open_other_field() {
+ yield PlacesUtils.keywords.insert({
+ url: TEST_URL,
+ keyword: "kw2",
+ postData: "search%3D%25s"
+ });
+ registerCleanupFunction(function* () {
+ yield PlacesUtils.keywords.remove("kw2");
+ });
+ // Reopening on another field of the same page that has different postData
+ // should not show the existing keyword.
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_URL,
+ }, function* (browser) {
+ // We must wait for the context menu code to build metadata.
+ yield openContextMenuForContentSelector(browser, '#form2 > input[name="search"]');
+
+ yield withBookmarksDialog(true, AddKeywordForSearchField, function* (dialogWin) {
+ let acceptBtn = dialogWin.document.documentElement.getButton("accept");
+ ok(acceptBtn.disabled, "Accept button is disabled");
+
+ let elt = dialogWin.document.getElementById("editBMPanel_keywordField");
+ is(elt.value, "");
+ });
+ });
+});
+
+function getPostDataString(stream) {
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"]
+ .createInstance(Ci.nsIScriptableInputStream);
+ sis.init(stream);
+ return sis.read(stream.available()).split("\n").pop();
+}
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_addLivemark.js b/browser/components/places/tests/browser/browser_bookmarkProperties_addLivemark.js
new file mode 100644
index 000000000..d9f4c07d7
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_addLivemark.js
@@ -0,0 +1,39 @@
+"use strict"
+
+add_task(function* () {
+ info("Add a live bookmark editing its data");
+
+ yield withSidebarTree("bookmarks", function* (tree) {
+ let itemId = PlacesUIUtils.leftPaneQueries["UnfiledBookmarks"];
+ tree.selectItems([itemId]);
+
+ yield withBookmarksDialog(
+ true,
+ function openDialog() {
+ PlacesCommandHook.addLiveBookmark("http://livemark.com/",
+ "livemark", "description");
+ },
+ function* test(dialogWin) {
+ let promiseTitleChangeNotification = promiseBookmarksNotification(
+ "onItemChanged", (itemId, prop, isAnno, val) => prop == "title" && val == "modified");
+
+ fillBookmarkTextField("editBMPanel_namePicker", "modified", dialogWin);
+
+ yield promiseTitleChangeNotification;
+
+ let bookmark = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX
+ });
+
+ is(bookmark.title, "modified", "folder name has been edited");
+
+ let livemark = yield PlacesUtils.livemarks.getLivemark({
+ guid: bookmark.guid
+ });
+ is(livemark.feedURI.spec, "http://livemark.com/", "livemark has the correct url");
+ is(livemark.title, "modified", "livemark has the correct title");
+ }
+ );
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js b/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js
new file mode 100644
index 000000000..fde9ea272
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js
@@ -0,0 +1,71 @@
+"use strict"
+
+add_task(function* () {
+ info("Bug 479348 - Properties on a root should be read-only.");
+ let uri = NetUtil.newURI("http://example.com/");
+ let bm = yield PlacesUtils.bookmarks.insert({
+ url: uri.spec,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ });
+ registerCleanupFunction(function* () {
+ yield PlacesUtils.bookmarks.remove(bm);
+ });
+
+ PlacesUtils.tagging.tagURI(uri, ["tag1"]);
+
+ let library = yield promiseLibrary();
+ let PlacesOrganizer = library.PlacesOrganizer;
+ registerCleanupFunction(function* () {
+ yield promiseLibraryClosed(library);
+ });
+
+ PlacesOrganizer.selectLeftPaneQuery("Tags");
+ let tree = PlacesOrganizer._places;
+ let tagsContainer = tree.selectedNode;
+ tagsContainer.containerOpen = true;
+ let fooTag = tagsContainer.getChild(0);
+ let tagNode = fooTag;
+ tree.selectNode(fooTag);
+ is(tagNode.title, 'tag1', "tagNode title is correct");
+
+ ok(tree.controller.isCommandEnabled("placesCmd_show:info"),
+ "'placesCmd_show:info' on current selected node is enabled");
+
+ yield withBookmarksDialog(
+ true,
+ function openDialog() {
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ function* test(dialogWin) {
+ // Check that the dialog is not read-only.
+ ok(!dialogWin.gEditItemOverlay.readOnly, "Dialog should not be read-only");
+
+ // Check that name picker is not read only
+ let namepicker = dialogWin.document.getElementById("editBMPanel_namePicker");
+ ok(!namepicker.readOnly, "Name field should not be read-only");
+ is(namepicker.value, "tag1", "Node title is correct");
+
+ let promiseTitleChangeNotification = promiseBookmarksNotification(
+ "onItemChanged", (itemId, prop, isAnno, val) => prop == "title" && val == "tag2");
+
+ fillBookmarkTextField("editBMPanel_namePicker", "tag2", dialogWin);
+
+ yield promiseTitleChangeNotification;
+
+ is(namepicker.value, "tag2", "Node title has been properly edited");
+
+ // Check the shortcut's title.
+ is(tree.selectedNode.title, "tag2", "The node has the correct title");
+
+ // Check the tags have been edited.
+ let tags = PlacesUtils.tagging.getTagsForURI(uri);
+ is(tags.length, 1, "Found the right number of tags");
+ ok(tags.includes("tag2"), "Found the expected tag");
+ }
+ );
+
+ // Check the tag change has been reverted.
+ let tags = PlacesUtils.tagging.getTagsForURI(uri);
+ is(tags.length, 1, "Found the right number of tags");
+ ok(tags.includes("tag1"), "Found the expected tag");
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js b/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js
new file mode 100644
index 000000000..6f499888c
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js
@@ -0,0 +1,42 @@
+"use strict"
+
+add_task(function* () {
+ info("Bug 479348 - Properties on a root should be read-only.");
+
+ yield withSidebarTree("bookmarks", function* (tree) {
+ let itemId = PlacesUIUtils.leftPaneQueries["UnfiledBookmarks"];
+ tree.selectItems([itemId]);
+ ok(tree.controller.isCommandEnabled("placesCmd_show:info"),
+ "'placesCmd_show:info' on current selected node is enabled");
+
+ yield withBookmarksDialog(
+ true,
+ function openDialog() {
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ function* test(dialogWin) {
+ // Check that the dialog is read-only.
+ ok(dialogWin.gEditItemOverlay.readOnly, "Dialog is read-only");
+ // Check that accept button is disabled
+ let acceptButton = dialogWin.document.documentElement.getButton("accept");
+ ok(acceptButton.disabled, "Accept button is disabled");
+
+ // Check that name picker is read only
+ let namepicker = dialogWin.document.getElementById("editBMPanel_namePicker");
+ ok(namepicker.readOnly, "Name field is read-only");
+ is(namepicker.value,
+ PlacesUtils.bookmarks.getItemTitle(PlacesUtils.unfiledBookmarksFolderId),
+ "Node title is correct");
+ // Blur the field and ensure root's name has not been changed.
+ namepicker.blur();
+ is(namepicker.value,
+ PlacesUtils.bookmarks.getItemTitle(PlacesUtils.unfiledBookmarksFolderId),
+ "Root title is correct");
+ // Check the shortcut's title.
+ let bookmark = yield PlacesUtils.bookmarks.fetch(tree.selectedNode.bookmarkGuid);
+ is(bookmark.title, null,
+ "Shortcut title is null");
+ }
+ );
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmark_all_tabs.js b/browser/components/places/tests/browser/browser_bookmark_all_tabs.js
new file mode 100644
index 000000000..afd32b78a
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_all_tabs.js
@@ -0,0 +1,37 @@
+/**
+ * Test for Bug 446171 - Name field of bookmarks saved via 'Bookmark All Tabs'
+ * has '(null)' value if history is disabled or just in private browsing mode
+ */
+"use strict"
+
+add_task(function* () {
+ const BASE_URL = "http://example.org/browser/browser/components/places/tests/browser/";
+ const TEST_PAGES = [
+ BASE_URL + "bookmark_dummy_1.html",
+ BASE_URL + "bookmark_dummy_2.html",
+ BASE_URL + "bookmark_dummy_1.html"
+ ];
+
+ function promiseAddTab(url) {
+ return BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ }
+
+ let tabs = yield Promise.all(TEST_PAGES.map(promiseAddTab));
+
+ let URIs = PlacesCommandHook.uniqueCurrentPages;
+ is(URIs.length, 3, "Only unique pages are returned");
+
+ Assert.deepEqual(URIs.map(URI => URI.uri.spec), [
+ "about:blank",
+ BASE_URL + "bookmark_dummy_1.html",
+ BASE_URL + "bookmark_dummy_2.html"
+ ], "Correct URIs are returned");
+
+ Assert.deepEqual(URIs.map(URI => URI.title), [
+ "New Tab", "Bookmark Dummy 1", "Bookmark Dummy 2"
+ ], "Correct titles are returned");
+
+ registerCleanupFunction(function* () {
+ yield Promise.all(tabs.map(BrowserTestUtils.removeTab));
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js b/browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js
new file mode 100644
index 000000000..85ce25311
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js
@@ -0,0 +1,61 @@
+"use strict";
+
+const TEST_URL = 'http://example.com/browser/browser/components/places/tests/browser/pageopeningwindow.html';
+
+function makeBookmarkFor(url, keyword) {
+ return Promise.all([
+ PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmarklet",
+ url: url }),
+ PlacesUtils.keywords.insert({url: url,
+ keyword: keyword})
+ ]);
+
+}
+
+add_task(function* openKeywordBookmarkWithWindowOpen() {
+ // This is the current default, but let's not assume that...
+ yield new Promise((resolve, reject) => {
+ SpecialPowers.pushPrefEnv({ 'set': [[ 'browser.link.open_newwindow', 3 ],
+ [ 'dom.disable_open_during_load', true ]] },
+ resolve);
+ });
+
+ let moztab;
+ let tabOpened = BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla")
+ .then((tab) => { moztab = tab; });
+ let keywordForBM = "openmeatab";
+
+ let bookmarkInfo;
+ let bookmarkCreated =
+ makeBookmarkFor("javascript:void open('" + TEST_URL + "')", keywordForBM)
+ .then((values) => {
+ bookmarkInfo = values[0];
+ });
+ yield Promise.all([tabOpened, bookmarkCreated]);
+
+ registerCleanupFunction(function() {
+ return Promise.all([
+ PlacesUtils.bookmarks.remove(bookmarkInfo),
+ PlacesUtils.keywords.remove(keywordForBM)
+ ]);
+ });
+ gURLBar.value = keywordForBM;
+ gURLBar.focus();
+
+ let tabCreatedPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ info("Waiting for tab being created");
+ let {target: tab} = yield tabCreatedPromise;
+ info("Got tab");
+ let browser = tab.linkedBrowser;
+ if (!browser.currentURI || browser.currentURI.spec != TEST_URL) {
+ info("Waiting for browser load");
+ yield BrowserTestUtils.browserLoaded(browser);
+ }
+ is(browser.currentURI && browser.currentURI.spec, TEST_URL, "Tab with expected URL loaded.");
+ info("Waiting to remove tab");
+ yield Promise.all([ BrowserTestUtils.removeTab(tab),
+ BrowserTestUtils.removeTab(moztab) ]);
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarksProperties.js b/browser/components/places/tests/browser/browser_bookmarksProperties.js
new file mode 100644
index 000000000..f7f9f4762
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarksProperties.js
@@ -0,0 +1,450 @@
+/* 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/. */
+
+/**
+ * Tests the bookmarks Properties dialog.
+ */
+
+// DOM ids of Places sidebar trees.
+const SIDEBAR_HISTORY_TREE_ID = "historyTree";
+const SIDEBAR_BOOKMARKS_TREE_ID = "bookmarks-view";
+
+const SIDEBAR_HISTORY_ID = "viewHistorySidebar";
+const SIDEBAR_BOOKMARKS_ID = "viewBookmarksSidebar";
+
+// For history sidebar.
+const SIDEBAR_HISTORY_BYLASTVISITED_VIEW = "bylastvisited";
+const SIDEBAR_HISTORY_BYMOSTVISITED_VIEW = "byvisited";
+const SIDEBAR_HISTORY_BYDATE_VIEW = "byday";
+const SIDEBAR_HISTORY_BYSITE_VIEW = "bysite";
+const SIDEBAR_HISTORY_BYDATEANDSITE_VIEW = "bydateandsite";
+
+// Action to execute on the current node.
+const ACTION_EDIT = 0;
+const ACTION_ADD = 1;
+
+// If action is ACTION_ADD, set type to one of those, to define what do you
+// want to create.
+const TYPE_FOLDER = 0;
+const TYPE_BOOKMARK = 1;
+
+const TEST_URL = "http://www.example.com/";
+
+const DIALOG_URL = "chrome://browser/content/places/bookmarkProperties.xul";
+const DIALOG_URL_MINIMAL_UI = "chrome://browser/content/places/bookmarkProperties2.xul";
+
+Cu.import("resource:///modules/RecentWindow.jsm");
+var win = RecentWindow.getMostRecentBrowserWindow();
+var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+
+function add_bookmark(aURI) {
+ var bId = PlacesUtils.bookmarks
+ .insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ aURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark/" + aURI.spec);
+ return bId;
+}
+
+// Each test is an obj w/ a desc property and run method.
+var gTests = [];
+var gCurrentTest = null;
+
+// ------------------------------------------------------------------------------
+// Bug 462662 - Pressing Enter to select tag from autocomplete closes bookmarks properties dialog
+gTests.push({
+ desc: "Bug 462662 - Pressing Enter to select tag from autocomplete closes bookmarks properties dialog",
+ sidebar: SIDEBAR_BOOKMARKS_ID,
+ action: ACTION_EDIT,
+ itemType: null,
+ window: null,
+ _itemId: null,
+ _cleanShutdown: false,
+
+ setup: function(aCallback) {
+ // Add a bookmark in unsorted bookmarks folder.
+ this._itemId = add_bookmark(PlacesUtils._uri(TEST_URL));
+ ok(this._itemId > 0, "Correctly added a bookmark");
+ // Add a tag to this bookmark.
+ PlacesUtils.tagging.tagURI(PlacesUtils._uri(TEST_URL),
+ ["testTag"]);
+ var tags = PlacesUtils.tagging.getTagsForURI(PlacesUtils._uri(TEST_URL));
+ is(tags[0], "testTag", "Correctly added a tag");
+ aCallback();
+ },
+
+ selectNode: function(tree) {
+ tree.selectItems([PlacesUtils.unfiledBookmarksFolderId]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ tree.selectItems([this._itemId]);
+ is(tree.selectedNode.itemId, this._itemId, "Bookmark has been selected");
+ },
+
+ run: function() {
+ // open tags autocomplete and press enter
+ var tagsField = this.window.document.getElementById("editBMPanel_tagsField");
+ var self = this;
+
+ this.window.addEventListener("unload", function(event) {
+ self.window.removeEventListener("unload", arguments.callee, true);
+ tagsField.popup.removeEventListener("popuphidden", popupListener, true);
+ ok(self._cleanShutdown, "Dialog window should not be closed by pressing Enter on the autocomplete popup");
+ executeSoon(function () {
+ self.finish();
+ });
+ }, true);
+
+ var popupListener = {
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "popuphidden":
+ // Everything worked fine, we can stop observing the window.
+ self._cleanShutdown = true;
+ self.window.document.documentElement.cancelDialog();
+ break;
+ case "popupshown":
+ tagsField.popup.removeEventListener("popupshown", this, true);
+ // In case this test fails the window will close, the test will fail
+ // since we didn't set _cleanShutdown.
+ var tree = tagsField.popup.tree;
+ // Focus and select first result.
+ isnot(tree, null, "Autocomplete results tree exists");
+ is(tree.view.rowCount, 1, "We have 1 autocomplete result");
+ tagsField.popup.selectedIndex = 0;
+ is(tree.view.selection.count, 1,
+ "We have selected a tag from the autocomplete popup");
+ info("About to focus the autocomplete results tree");
+ tree.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, self.window);
+ break;
+ default:
+ ok(false, "unknown event: " + aEvent.type);
+ return;
+ }
+ }
+ };
+ tagsField.popup.addEventListener("popupshown", popupListener, true);
+ tagsField.popup.addEventListener("popuphidden", popupListener, true);
+
+ // Open tags autocomplete popup.
+ info("About to focus the tagsField");
+ executeSoon(() => {
+ tagsField.focus();
+ tagsField.value = "";
+ EventUtils.synthesizeKey("t", {}, this.window);
+ });
+ },
+
+ finish: function() {
+ SidebarUI.hide();
+ runNextTest();
+ },
+
+ cleanup: function() {
+ // Check tags have not changed.
+ var tags = PlacesUtils.tagging.getTagsForURI(PlacesUtils._uri(TEST_URL));
+ is(tags[0], "testTag", "Tag on node has not changed");
+
+ // Cleanup.
+ PlacesUtils.tagging.untagURI(PlacesUtils._uri(TEST_URL), ["testTag"]);
+ PlacesUtils.bookmarks.removeItem(this._itemId);
+ }
+});
+
+// ------------------------------------------------------------------------------
+// Bug 476020 - Pressing Esc while having the tag autocomplete open closes the bookmarks panel
+
+gTests.push({
+ desc: "Bug 476020 - Pressing Esc while having the tag autocomplete open closes the bookmarks panel",
+ sidebar: SIDEBAR_BOOKMARKS_ID,
+ action: ACTION_EDIT,
+ itemType: null,
+ window: null,
+ _itemId: null,
+ _cleanShutdown: false,
+
+ setup: function(aCallback) {
+ // Add a bookmark in unsorted bookmarks folder.
+ this._itemId = add_bookmark(PlacesUtils._uri(TEST_URL));
+ ok(this._itemId > 0, "Correctly added a bookmark");
+ // Add a tag to this bookmark.
+ PlacesUtils.tagging.tagURI(PlacesUtils._uri(TEST_URL),
+ ["testTag"]);
+ var tags = PlacesUtils.tagging.getTagsForURI(PlacesUtils._uri(TEST_URL));
+ is(tags[0], "testTag", "Correctly added a tag");
+ aCallback();
+ },
+
+ selectNode: function(tree) {
+ tree.selectItems([PlacesUtils.unfiledBookmarksFolderId]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ tree.selectItems([this._itemId]);
+ is(tree.selectedNode.itemId, this._itemId, "Bookmark has been selected");
+ },
+
+ run: function() {
+ // open tags autocomplete and press enter
+ var tagsField = this.window.document.getElementById("editBMPanel_tagsField");
+ var self = this;
+
+ this.window.addEventListener("unload", function(event) {
+ self.window.removeEventListener("unload", arguments.callee, true);
+ tagsField.popup.removeEventListener("popuphidden", popupListener, true);
+ ok(self._cleanShutdown, "Dialog window should not be closed by pressing Escape on the autocomplete popup");
+ executeSoon(function () {
+ self.finish();
+ });
+ }, true);
+
+ var popupListener = {
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "popuphidden":
+ // Everything worked fine.
+ self._cleanShutdown = true;
+ self.window.document.documentElement.cancelDialog();
+ break;
+ case "popupshown":
+ tagsField.popup.removeEventListener("popupshown", this, true);
+ // In case this test fails the window will close, the test will fail
+ // since we didn't set _cleanShutdown.
+ var tree = tagsField.popup.tree;
+ // Focus and select first result.
+ isnot(tree, null, "Autocomplete results tree exists");
+ is(tree.view.rowCount, 1, "We have 1 autocomplete result");
+ tagsField.popup.selectedIndex = 0;
+ is(tree.view.selection.count, 1,
+ "We have selected a tag from the autocomplete popup");
+ info("About to focus the autocomplete results tree");
+ tree.focus();
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, self.window);
+ break;
+ default:
+ ok(false, "unknown event: " + aEvent.type);
+ return;
+ }
+ }
+ };
+ tagsField.popup.addEventListener("popupshown", popupListener, true);
+ tagsField.popup.addEventListener("popuphidden", popupListener, true);
+
+ // Open tags autocomplete popup.
+ info("About to focus the tagsField");
+ tagsField.focus();
+ tagsField.value = "";
+ EventUtils.synthesizeKey("t", {}, this.window);
+ },
+
+ finish: function() {
+ SidebarUI.hide();
+ runNextTest();
+ },
+
+ cleanup: function() {
+ // Check tags have not changed.
+ var tags = PlacesUtils.tagging.getTagsForURI(PlacesUtils._uri(TEST_URL));
+ is(tags[0], "testTag", "Tag on node has not changed");
+
+ // Cleanup.
+ PlacesUtils.tagging.untagURI(PlacesUtils._uri(TEST_URL),
+ ["testTag"]);
+ PlacesUtils.bookmarks.removeItem(this._itemId);
+ }
+});
+
+// ------------------------------------------------------------------------------
+// Bug 491269 - Test that editing folder name in bookmarks properties dialog does not accept the dialog
+
+gTests.push({
+ desc: " Bug 491269 - Test that editing folder name in bookmarks properties dialog does not accept the dialog",
+ sidebar: SIDEBAR_HISTORY_ID,
+ action: ACTION_ADD,
+ historyView: SIDEBAR_HISTORY_BYLASTVISITED_VIEW,
+ window: null,
+
+ setup: function(aCallback) {
+ // Add a visit.
+ PlacesTestUtils.addVisits(
+ {uri: PlacesUtils._uri(TEST_URL),
+ transition: PlacesUtils.history.TRANSITION_TYPED}
+ ).then(aCallback);
+ },
+
+ selectNode: function(tree) {
+ var visitNode = tree.view.nodeForTreeIndex(0);
+ tree.selectNode(visitNode);
+ is(tree.selectedNode.uri, TEST_URL, "The correct visit has been selected");
+ is(tree.selectedNode.itemId, -1, "The selected node is not bookmarked");
+ },
+
+ run: function() {
+ // Open folder selector.
+ var foldersExpander = this.window.document.getElementById("editBMPanel_foldersExpander");
+ var folderTree = this.window.document.getElementById("editBMPanel_folderTree");
+ var self = this;
+
+ this.window.addEventListener("unload", function(event) {
+ self.window.removeEventListener("unload", arguments.callee, true);
+ ok(self._cleanShutdown, "Dialog window should not be closed by pressing ESC in folder name textbox");
+ executeSoon(function () {
+ self.finish();
+ });
+ }, true);
+
+ folderTree.addEventListener("DOMAttrModified", function onDOMAttrModified(event) {
+ if (event.attrName != "place")
+ return;
+ folderTree.removeEventListener("DOMAttrModified", arguments.callee, false);
+ executeSoon(function () {
+ // Create a new folder.
+ var newFolderButton = self.window.document.getElementById("editBMPanel_newFolderButton");
+ newFolderButton.doCommand();
+ ok(folderTree.hasAttribute("editing"),
+ "We are editing new folder name in folder tree");
+
+ // Press Escape to discard editing new folder name.
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, self.window);
+ ok(!folderTree.hasAttribute("editing"),
+ "We have finished editing folder name in folder tree");
+ self._cleanShutdown = true;
+ self.window.document.documentElement.cancelDialog();
+ });
+ }, false);
+ foldersExpander.doCommand();
+ },
+
+ finish: function() {
+ SidebarUI.hide();
+ runNextTest();
+ },
+
+ cleanup: function() {
+ return PlacesTestUtils.clearHistory();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+function test() {
+ waitForExplicitFinish();
+ // This test can take some time, if we timeout too early it could run
+ // in the middle of other tests, or hang them.
+ requestLongerTimeout(2);
+
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils in context");
+ ok(PlacesUIUtils, "PlacesUIUtils in context");
+
+ // kick off tests
+ runNextTest();
+}
+
+function runNextTest() {
+ // Cleanup from previous test.
+ if (gCurrentTest) {
+ Promise.resolve(gCurrentTest.cleanup()).then(() => {
+ info("End of test: " + gCurrentTest.desc);
+ gCurrentTest = null;
+ waitForAsyncUpdates(runNextTest);
+ });
+ return;
+ }
+
+ if (gTests.length > 0) {
+ // Goto next tests.
+ gCurrentTest = gTests.shift();
+ info("Start of test: " + gCurrentTest.desc);
+ gCurrentTest.setup(function() {
+ execute_test_in_sidebar();
+ });
+ }
+ else {
+ // Finished all tests.
+ finish();
+ }
+}
+
+/**
+ * Global functions to run a test in Properties dialog context.
+ */
+
+function execute_test_in_sidebar() {
+ var sidebar = document.getElementById("sidebar");
+ sidebar.addEventListener("load", function() {
+ sidebar.removeEventListener("load", arguments.callee, true);
+ // Need to executeSoon since the tree is initialized on sidebar load.
+ executeSoon(open_properties_dialog);
+ }, true);
+ SidebarUI.show(gCurrentTest.sidebar);
+}
+
+function open_properties_dialog() {
+ var sidebar = document.getElementById("sidebar");
+
+ // If this is history sidebar, set the required view.
+ if (gCurrentTest.sidebar == SIDEBAR_HISTORY_ID)
+ sidebar.contentDocument.getElementById(gCurrentTest.historyView).doCommand();
+
+ // Get sidebar's Places tree.
+ var sidebarTreeID = gCurrentTest.sidebar == SIDEBAR_BOOKMARKS_ID ?
+ SIDEBAR_BOOKMARKS_TREE_ID :
+ SIDEBAR_HISTORY_TREE_ID;
+ var tree = sidebar.contentDocument.getElementById(sidebarTreeID);
+ ok(tree, "Sidebar tree has been loaded");
+
+ // Ask current test to select the node to edit.
+ gCurrentTest.selectNode(tree);
+ ok(tree.selectedNode,
+ "We have a places node selected: " + tree.selectedNode.title);
+
+ // Wait for the Properties dialog.
+ function windowObserver(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowopened")
+ return;
+ ww.unregisterNotification(windowObserver);
+ let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ waitForFocus(() => {
+ // Windows has been loaded, execute our test now.
+ executeSoon(function () {
+ // Ensure overlay is loaded
+ ok(win.gEditItemOverlay.initialized, "EditItemOverlay is initialized");
+ gCurrentTest.window = win;
+ try {
+ gCurrentTest.run();
+ } catch (ex) {
+ ok(false, "An error occured during test run: " + ex.message);
+ }
+ });
+ }, win);
+ }
+ ww.registerNotification(windowObserver);
+
+ var command = null;
+ switch (gCurrentTest.action) {
+ case ACTION_EDIT:
+ command = "placesCmd_show:info";
+ break;
+ case ACTION_ADD:
+ if (gCurrentTest.sidebar == SIDEBAR_BOOKMARKS_ID) {
+ if (gCurrentTest.itemType == TYPE_FOLDER)
+ command = "placesCmd_new:folder";
+ else if (gCurrentTest.itemType == TYPE_BOOKMARK)
+ command = "placesCmd_new:bookmark";
+ else
+ ok(false, "You didn't set a valid itemType for adding an item");
+ }
+ else
+ command = "placesCmd_createBookmark";
+ break;
+ default:
+ ok(false, "You didn't set a valid action for this test");
+ }
+ // Ensure command is enabled for this node.
+ ok(tree.controller.isCommandEnabled(command),
+ " command '" + command + "' on current selected node is enabled");
+
+ // This will open the dialog.
+ tree.controller.doCommand(command);
+}
diff --git a/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js b/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js
new file mode 100644
index 000000000..1ab9411f3
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js
@@ -0,0 +1,256 @@
+/* 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 TEST_URL = "http://www.mozilla.org";
+const TEST_TITLE = "example_title";
+
+var gBookmarksToolbar = window.document.getElementById("PlacesToolbar");
+var dragDirections = { LEFT: 0, UP: 1, RIGHT: 2, DOWN: 3 };
+
+/**
+ * Tests dragging on toolbar.
+ *
+ * We must test these 2 cases:
+ * - Dragging toward left, top, right should start a drag.
+ * - Dragging toward down should should open the container if the item is a
+ * container, drag the item otherwise.
+ *
+ * @param aElement
+ * DOM node element we will drag
+ * @param aExpectedDragData
+ * Array of flavors and values in the form:
+ * [ ["text/plain: sometext", "text/html: <b>sometext</b>"], [...] ]
+ * Pass an empty array to check that drag even has been canceled.
+ * @param aDirection
+ * Direction for the dragging gesture, see dragDirections helper object.
+ */
+function synthesizeDragWithDirection(aElement, aExpectedDragData, aDirection, aCallback) {
+ // Dragstart listener function.
+ gBookmarksToolbar.addEventListener("dragstart", function(event)
+ {
+ info("A dragstart event has been trapped.");
+ var dataTransfer = event.dataTransfer;
+ is(dataTransfer.mozItemCount, aExpectedDragData.length,
+ "Number of dragged items should be the same.");
+
+ for (var t = 0; t < dataTransfer.mozItemCount; t++) {
+ var types = dataTransfer.mozTypesAt(t);
+ var expecteditem = aExpectedDragData[t];
+ is(types.length, expecteditem.length,
+ "Number of flavors for item " + t + " should be the same.");
+
+ for (var f = 0; f < types.length; f++) {
+ is(types[f], expecteditem[f].substring(0, types[f].length),
+ "Flavor " + types[f] + " for item " + t + " should be the same.");
+ is(dataTransfer.mozGetDataAt(types[f], t),
+ expecteditem[f].substring(types[f].length + 2),
+ "Contents for item " + t + " with flavor " + types[f] + " should be the same.");
+ }
+ }
+
+ if (!aExpectedDragData.length)
+ ok(event.defaultPrevented, "Drag has been canceled.");
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ gBookmarksToolbar.removeEventListener("dragstart", arguments.callee, false);
+
+ // This is likely to cause a click event, and, in case we are dragging a
+ // bookmark, an unwanted page visit. Prevent the click event.
+ aElement.addEventListener("click", prevent, false);
+ EventUtils.synthesizeMouse(aElement,
+ startingPoint.x + xIncrement * 9,
+ startingPoint.y + yIncrement * 9,
+ { type: "mouseup" });
+ aElement.removeEventListener("click", prevent, false);
+
+ // Cleanup eventually opened menus.
+ if (aElement.localName == "menu" && aElement.open)
+ aElement.open = false;
+ aCallback()
+ }, false);
+
+ var prevent = function(aEvent) { aEvent.preventDefault(); }
+
+ var xIncrement = 0;
+ var yIncrement = 0;
+
+ switch (aDirection) {
+ case dragDirections.LEFT:
+ xIncrement = -1;
+ break;
+ case dragDirections.RIGHT:
+ xIncrement = +1;
+ break;
+ case dragDirections.UP:
+ yIncrement = -1;
+ break;
+ case dragDirections.DOWN:
+ yIncrement = +1;
+ break;
+ }
+
+ var rect = aElement.getBoundingClientRect();
+ var startingPoint = { x: (rect.right - rect.left)/2,
+ y: (rect.bottom - rect.top)/2 };
+
+ EventUtils.synthesizeMouse(aElement,
+ startingPoint.x,
+ startingPoint.y,
+ { type: "mousedown" });
+ EventUtils.synthesizeMouse(aElement,
+ startingPoint.x + xIncrement * 1,
+ startingPoint.y + yIncrement * 1,
+ { type: "mousemove" });
+ EventUtils.synthesizeMouse(aElement,
+ startingPoint.x + xIncrement * 9,
+ startingPoint.y + yIncrement * 9,
+ { type: "mousemove" });
+}
+
+function getToolbarNodeForItemId(aItemId) {
+ var children = document.getElementById("PlacesToolbarItems").childNodes;
+ var node = null;
+ for (var i = 0; i < children.length; i++) {
+ if (aItemId == children[i]._placesNode.itemId) {
+ node = children[i];
+ break;
+ }
+ }
+ return node;
+}
+
+function getExpectedDataForPlacesNode(aNode) {
+ var wrappedNode = [];
+ var flavors = ["text/x-moz-place",
+ "text/x-moz-url",
+ "text/plain",
+ "text/html"];
+
+ flavors.forEach(function(aFlavor) {
+ var wrappedFlavor = aFlavor + ": " +
+ PlacesUtils.wrapNode(aNode, aFlavor);
+ wrappedNode.push(wrappedFlavor);
+ });
+
+ return [wrappedNode];
+}
+
+var gTests = [
+
+// ------------------------------------------------------------------------------
+
+ {
+ desc: "Drag a folder on toolbar",
+ run: function() {
+ // Create a test folder to be dragged.
+ var folderId = PlacesUtils.bookmarks
+ .createFolder(PlacesUtils.toolbarFolderId,
+ TEST_TITLE,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ var element = getToolbarNodeForItemId(folderId);
+ isnot(element, null, "Found node on toolbar");
+
+ isnot(element._placesNode, null, "Toolbar node has an associated Places node.");
+ var expectedData = getExpectedDataForPlacesNode(element._placesNode);
+
+ info("Dragging left");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.LEFT,
+ function ()
+ {
+ info("Dragging right");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.RIGHT,
+ function ()
+ {
+ info("Dragging up");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.UP,
+ function ()
+ {
+ info("Dragging down");
+ synthesizeDragWithDirection(element, new Array(), dragDirections.DOWN,
+ function () {
+ // Cleanup.
+ PlacesUtils.bookmarks.removeItem(folderId);
+ nextTest();
+ });
+ });
+ });
+ });
+ }
+ },
+
+// ------------------------------------------------------------------------------
+
+ {
+ desc: "Drag a bookmark on toolbar",
+ run: function() {
+ // Create a test bookmark to be dragged.
+ var itemId = PlacesUtils.bookmarks
+ .insertBookmark(PlacesUtils.toolbarFolderId,
+ PlacesUtils._uri(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TEST_TITLE);
+ var element = getToolbarNodeForItemId(itemId);
+ isnot(element, null, "Found node on toolbar");
+
+ isnot(element._placesNode, null, "Toolbar node has an associated Places node.");
+ var expectedData = getExpectedDataForPlacesNode(element._placesNode);
+
+ info("Dragging left");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.LEFT,
+ function ()
+ {
+ info("Dragging right");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.RIGHT,
+ function ()
+ {
+ info("Dragging up");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.UP,
+ function ()
+ {
+ info("Dragging down");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.DOWN,
+ function () {
+ // Cleanup.
+ PlacesUtils.bookmarks.removeItem(itemId);
+ nextTest();
+ });
+ });
+ });
+ });
+ }
+ },
+];
+
+function nextTest() {
+ if (gTests.length) {
+ var test = gTests.shift();
+ waitForFocus(function() {
+ info("Start of test: " + test.desc);
+ test.run();
+ });
+ }
+ else if (wasCollapsed) {
+ // Collapse the personal toolbar if needed.
+ promiseSetToolbarVisibility(toolbar, false).then(finish);
+ } else {
+ finish();
+ }
+}
+
+var toolbar = document.getElementById("PersonalToolbar");
+var wasCollapsed = toolbar.collapsed;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ promiseSetToolbarVisibility(toolbar, true).then(nextTest);
+ } else {
+ nextTest();
+ }
+}
+
diff --git a/browser/components/places/tests/browser/browser_forgetthissite_single.js b/browser/components/places/tests/browser/browser_forgetthissite_single.js
new file mode 100644
index 000000000..b1d7936e9
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_forgetthissite_single.js
@@ -0,0 +1,78 @@
+/* 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/. */
+
+"use strict";
+
+const TEST_URIs = [
+ "http://www.mozilla.org/test1",
+ "http://www.mozilla.org/test2"
+];
+
+// This test makes sure that the Forget This Site command is hidden for multiple
+// selections.
+add_task(function* () {
+ // Add a history entry.
+ ok(PlacesUtils, "checking PlacesUtils, running in chrome context?");
+
+ let places = [];
+ let transition = PlacesUtils.history.TRANSITION_TYPED;
+ TEST_URIs.forEach(uri => places.push({uri: PlacesUtils._uri(uri), transition}));
+
+ yield PlacesTestUtils.addVisits(places);
+ yield testForgetThisSiteVisibility(1);
+ yield testForgetThisSiteVisibility(2);
+
+ // Cleanup.
+ yield PlacesTestUtils.clearHistory();
+});
+
+var testForgetThisSiteVisibility = Task.async(function* (selectionCount) {
+ let organizer = yield promiseLibrary();
+
+ // Select History in the left pane.
+ organizer.PlacesOrganizer.selectLeftPaneQuery("History");
+ let PO = organizer.PlacesOrganizer;
+ let histContainer = PO._places.selectedNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ histContainer.containerOpen = true;
+ PO._places.selectNode(histContainer.getChild(0));
+
+ // Select the first history entry.
+ let doc = organizer.document;
+ let tree = doc.getElementById("placeContent");
+ let selection = tree.view.selection;
+ selection.clearSelection();
+ selection.rangedSelect(0, selectionCount - 1, true);
+ is(selection.count, selectionCount, "The selected range is as big as expected");
+
+ // Open the context menu.
+ let contextmenu = doc.getElementById("placesContext");
+ let popupShown = promisePopupShown(contextmenu);
+
+ // Get cell coordinates.
+ let rect = tree.treeBoxObject.getCoordsForCellItem(0, tree.columns[0], "text");
+ // Initiate a context menu for the selected cell.
+ EventUtils.synthesizeMouse(tree.body, rect.x + rect.width / 2, rect.y + rect.height / 2, {type: "contextmenu", button: 2}, organizer);
+ yield popupShown;
+
+ let forgetThisSite = doc.getElementById("placesContext_deleteHost");
+ let hideForgetThisSite = (selectionCount != 1);
+ is(forgetThisSite.hidden, hideForgetThisSite,
+ `The Forget this site menu item should ${hideForgetThisSite ? "" : "not "}` +
+ ` be hidden with ${selectionCount} items selected`);
+
+ // Close the context menu.
+ contextmenu.hidePopup();
+
+ // Close the library window.
+ yield promiseLibraryClosed(organizer);
+});
+
+function promisePopupShown(popup) {
+ return new Promise(resolve => {
+ popup.addEventListener("popupshown", function onShown() {
+ popup.removeEventListener("popupshown", onShown, true);
+ resolve();
+ }, true);
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_history_sidebar_search.js b/browser/components/places/tests/browser/browser_history_sidebar_search.js
new file mode 100644
index 000000000..89472c4ab
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_history_sidebar_search.js
@@ -0,0 +1,64 @@
+add_task(function* test () {
+ let sidebar = document.getElementById("sidebar");
+
+ // Visited pages listed by descending visit date.
+ let pages = [
+ "http://sidebar.mozilla.org/a",
+ "http://sidebar.mozilla.org/b",
+ "http://sidebar.mozilla.org/c",
+ "http://www.mozilla.org/d",
+ ];
+
+ // Number of pages that will be filtered out by the search.
+ const FILTERED_COUNT = 1;
+
+ yield PlacesTestUtils.clearHistory();
+
+ // Add some visited page.
+ let time = Date.now();
+ let places = [];
+ for (let i = 0; i < pages.length; i++) {
+ places.push({ uri: NetUtil.newURI(pages[i]),
+ visitDate: (time - i) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED });
+ }
+ yield PlacesTestUtils.addVisits(places);
+
+ yield withSidebarTree("history", function* () {
+ info("Set 'by last visited' view");
+ sidebar.contentDocument.getElementById("bylastvisited").doCommand();
+ let tree = sidebar.contentDocument.getElementById("historyTree");
+ check_tree_order(tree, pages);
+
+ // Set a search value.
+ let searchBox = sidebar.contentDocument.getElementById("search-box");
+ ok(searchBox, "search box is in context");
+ searchBox.value = "sidebar.mozilla";
+ searchBox.doCommand();
+ check_tree_order(tree, pages, -FILTERED_COUNT);
+
+ info("Reset the search");
+ searchBox.value = "";
+ searchBox.doCommand();
+ check_tree_order(tree, pages);
+ });
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+function check_tree_order(tree, pages, aNumberOfRowsDelta = 0) {
+ let treeView = tree.view;
+ let columns = tree.columns;
+ is(columns.count, 1, "There should be only 1 column in the sidebar");
+
+ let found = 0;
+ for (let i = 0; i < treeView.rowCount; i++) {
+ let node = treeView.nodeForTreeIndex(i);
+ // We could inherit delayed visits from previous tests, skip them.
+ if (!pages.includes(node.uri))
+ continue;
+ is(node.uri, pages[i], "Node is in correct position based on its visit date");
+ found++;
+ }
+ ok(found, pages.length + aNumberOfRowsDelta, "Found all expected results");
+}
diff --git a/browser/components/places/tests/browser/browser_library_batch_delete.js b/browser/components/places/tests/browser/browser_library_batch_delete.js
new file mode 100644
index 000000000..6a907c70f
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_batch_delete.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that Library handles correctly batch deletes.
+ */
+
+const TEST_URL = "http://www.batch.delete.me/";
+
+var gTests = [];
+var gLibrary;
+
+// ------------------------------------------------------------------------------
+
+gTests.push({
+ desc: "Create and batch remove bookmarks",
+ run: function() {
+ let testURI = makeURI(TEST_URL);
+ PlacesUtils.history.runInBatchMode({
+ runBatched: function (aUserData) {
+ // Create a folder in unserted and populate it with bookmarks.
+ let folder = PlacesUtils.bookmarks.createFolder(
+ PlacesUtils.unfiledBookmarksFolderId, "deleteme",
+ PlacesUtils.bookmarks.DEFAULT_INDEX
+ );
+ PlacesUtils.bookmarks.createFolder(
+ PlacesUtils.unfiledBookmarksFolderId, "keepme",
+ PlacesUtils.bookmarks.DEFAULT_INDEX
+ );
+ for (let i = 0; i < 10; i++) {
+ PlacesUtils.bookmarks.insertBookmark(folder,
+ testURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bm" + i);
+ }
+ }
+ }, null);
+
+ // Select and open the left pane "History" query.
+ let PO = gLibrary.PlacesOrganizer;
+ PO.selectLeftPaneQuery("UnfiledBookmarks");
+ isnot(PO._places.selectedNode, null, "Selected unsorted bookmarks");
+
+ let unsortedNode = PlacesUtils.asContainer(PO._places.selectedNode);
+ unsortedNode.containerOpen = true;
+ is(unsortedNode.childCount, 2, "Unsorted node has 2 children");
+ let folderNode = unsortedNode.getChild(0);
+ is(folderNode.title, "deleteme", "Folder found in unsorted bookmarks");
+ // Check delete command is available.
+ PO._places.selectNode(folderNode);
+ is(PO._places.selectedNode.title, "deleteme", "Folder node selected");
+ ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled");
+ // Execute the delete command and check bookmark has been removed.
+ PO._places.controller.doCommand("cmd_delete");
+ ok(!PlacesUtils.bookmarks.isBookmarked(testURI),
+ "Bookmark has been correctly removed");
+ // Test live update.
+ is(unsortedNode.childCount, 1, "Unsorted node has 1 child");
+ is(PO._places.selectedNode.title, "keepme", "Folder node selected");
+ unsortedNode.containerOpen = false;
+ nextTest();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+gTests.push({
+ desc: "Ensure correct selection and functionality in Library",
+ run: function() {
+ let PO = gLibrary.PlacesOrganizer;
+ let ContentTree = gLibrary.ContentTree;
+ // Move selection forth and back.
+ PO.selectLeftPaneQuery("History");
+ PO.selectLeftPaneQuery("UnfiledBookmarks");
+ // Now select the "keepme" folder in the right pane and delete it.
+ ContentTree.view.selectNode(ContentTree.view.result.root.getChild(0));
+ is(ContentTree.view.selectedNode.title, "keepme",
+ "Found folder in content pane");
+ // Test live update.
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ makeURI(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bm");
+ is(ContentTree.view.result.root.childCount, 2,
+ "Right pane was correctly updated");
+ nextTest();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+function test() {
+ waitForExplicitFinish();
+ registerCleanupFunction(function () {
+ PlacesUtils.bookmarks
+ .removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ });
+
+ gLibrary = openLibrary(nextTest);
+}
+
+function nextTest() {
+ if (gTests.length) {
+ var test = gTests.shift();
+ info("Start of test: " + test.desc);
+ test.run();
+ }
+ else {
+ // Close Library window.
+ gLibrary.close();
+ finish();
+ }
+}
diff --git a/browser/components/places/tests/browser/browser_library_commands.js b/browser/components/places/tests/browser/browser_library_commands.js
new file mode 100644
index 000000000..e3bb75a34
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_commands.js
@@ -0,0 +1,235 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Test enabled commands in the left pane folder of the Library.
+ */
+
+const TEST_URI = NetUtil.newURI("http://www.mozilla.org/");
+
+registerCleanupFunction(function* () {
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_date_container() {
+ let library = yield promiseLibrary();
+ info("Ensure date containers under History cannot be cut but can be deleted");
+
+ yield PlacesTestUtils.addVisits(TEST_URI);
+
+ // Select and open the left pane "History" query.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneQuery('History');
+ isnot(PO._places.selectedNode, null, "We correctly selected History");
+
+ // Check that both delete and cut commands are disabled, cause this is
+ // a child of the left pane folder.
+ ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is disabled");
+ let historyNode = PlacesUtils.asContainer(PO._places.selectedNode);
+ historyNode.containerOpen = true;
+
+ // Check that we have a child container. It is "Today" container.
+ is(historyNode.childCount, 1, "History node has one child");
+ let todayNode = historyNode.getChild(0);
+ let todayNodeExpectedTitle = PlacesUtils.getString("finduri-AgeInDays-is-0");
+ is(todayNode.title, todayNodeExpectedTitle,
+ "History child is the expected container");
+
+ // Select "Today" container.
+ PO._places.selectNode(todayNode);
+ is(PO._places.selectedNode, todayNode,
+ "We correctly selected Today container");
+ // Check that delete command is enabled but cut command is disabled, cause
+ // this is an history item.
+ ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled");
+ ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled");
+
+ // Execute the delete command and check visit has been removed.
+ let promiseURIRemoved = promiseHistoryNotification("onDeleteURI",
+ v => TEST_URI.equals(v));
+ PO._places.controller.doCommand("cmd_delete");
+ yield promiseURIRemoved;
+
+ // Test live update of "History" query.
+ is(historyNode.childCount, 0, "History node has no more children");
+
+ historyNode.containerOpen = false;
+
+ ok(!(yield promiseIsURIVisited(TEST_URI)), "Visit has been removed");
+
+ library.close();
+});
+
+add_task(function* test_query_on_toolbar() {
+ let library = yield promiseLibrary();
+ info("Ensure queries can be cut or deleted");
+
+ // Select and open the left pane "Bookmarks Toolbar" folder.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneQuery('BookmarksToolbar');
+ isnot(PO._places.selectedNode, null, "We have a valid selection");
+ is(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
+ PlacesUtils.toolbarFolderId,
+ "We have correctly selected bookmarks toolbar node.");
+
+ // Check that both cut and delete commands are disabled, cause this is a child
+ // of AllBookmarksFolderId.
+ ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is disabled");
+
+ let toolbarNode = PlacesUtils.asContainer(PO._places.selectedNode);
+ toolbarNode.containerOpen = true;
+
+ // Add an History query to the toolbar.
+ let query = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "place:sort=4",
+ title: "special_query",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0 });
+
+ // Get first child and check it is the just inserted query.
+ ok(toolbarNode.childCount > 0, "Toolbar node has children");
+ let queryNode = toolbarNode.getChild(0);
+ is(queryNode.title, "special_query", "Query node is correctly selected");
+
+ // Select query node.
+ PO._places.selectNode(queryNode);
+ is(PO._places.selectedNode, queryNode, "We correctly selected query node");
+
+ // Check that both cut and delete commands are enabled.
+ ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is enabled");
+ ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled");
+
+ // Execute the delete command and check bookmark has been removed.
+ let promiseItemRemoved = promiseBookmarksNotification("onItemRemoved",
+ (...args) => query.guid == args[5]);
+ PO._places.controller.doCommand("cmd_delete");
+ yield promiseItemRemoved;
+
+ is((yield PlacesUtils.bookmarks.fetch(query.guid)), null,
+ "Query node bookmark has been correctly removed");
+
+ toolbarNode.containerOpen = false;
+
+ library.close();
+});
+
+add_task(function* test_search_contents() {
+ yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "example page",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 0 });
+
+ let library = yield promiseLibrary();
+ info("Ensure query contents can be cut or deleted");
+
+ // Select and open the left pane "Bookmarks Toolbar" folder.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneQuery('BookmarksToolbar');
+ isnot(PO._places.selectedNode, null, "We have a valid selection");
+ is(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
+ PlacesUtils.toolbarFolderId,
+ "We have correctly selected bookmarks toolbar node.");
+
+ let searchBox = library.document.getElementById("searchFilter");
+ searchBox.value = "example";
+ library.PlacesSearchBox.search(searchBox.value);
+
+ let bookmarkNode = library.ContentTree.view.selectedNode;
+ is(bookmarkNode.uri, "http://example.com/", "Found the expected bookmark");
+
+ // Check that both cut and delete commands are enabled.
+ ok(library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is enabled");
+ ok(library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled");
+
+ library.close();
+});
+
+add_task(function* test_tags() {
+ yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "example page",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 0 });
+ PlacesUtils.tagging.tagURI(NetUtil.newURI("http://example.com/"), ["test"]);
+
+ let library = yield promiseLibrary();
+ info("Ensure query contents can be cut or deleted");
+
+ // Select and open the left pane "Bookmarks Toolbar" folder.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneQuery('Tags');
+ let tagsNode = PO._places.selectedNode;
+ isnot(tagsNode, null, "We have a valid selection");
+ let tagsTitle = PlacesUtils.getString("TagsFolderTitle");
+ is(tagsNode.title, tagsTitle,
+ "Tags has been properly selected");
+
+ // Check that both cut and delete commands are disabled.
+ ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is disabled");
+
+ // Now select the tag.
+ PlacesUtils.asContainer(tagsNode).containerOpen = true;
+ let tag = tagsNode.getChild(0);
+ PO._places.selectNode(tag);
+ is(PO._places.selectedNode.title, "test",
+ "The created tag has been properly selected");
+
+ // Check that cut is disabled but delete is enabled.
+ ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled");
+ ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled");
+
+ let bookmarkNode = library.ContentTree.view.selectedNode;
+ is(bookmarkNode.uri, "http://example.com/", "Found the expected bookmark");
+
+ // Check that both cut and delete commands are enabled.
+ ok(library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(!library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled");
+ ok(library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled");
+
+ tagsNode.containerOpen = false;
+
+ library.close();
+});
diff --git a/browser/components/places/tests/browser/browser_library_downloads.js b/browser/components/places/tests/browser/browser_library_downloads.js
new file mode 100644
index 000000000..81daadd71
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_downloads.js
@@ -0,0 +1,70 @@
+/* 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/. */
+
+/*
+ * Tests bug 564900: Add folder specifically for downloads to Library left pane.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=564900
+ * This test visits various pages then opens the Library and ensures
+ * that both the Downloads folder shows up and that the correct visits
+ * are shown in it.
+ */
+
+var now = Date.now();
+
+function test() {
+ waitForExplicitFinish();
+
+ let onLibraryReady = function(win) {
+ // Add visits to compare contents with.
+ let places = [
+ { uri: NetUtil.newURI("http://mozilla.com"),
+ visits: [ new VisitInfo(PlacesUtils.history.TRANSITION_TYPED) ]
+ },
+ { uri: NetUtil.newURI("http://google.com"),
+ visits: [ new VisitInfo(PlacesUtils.history.TRANSITION_DOWNLOAD) ]
+ },
+ { uri: NetUtil.newURI("http://en.wikipedia.org"),
+ visits: [ new VisitInfo(PlacesUtils.history.TRANSITION_TYPED) ]
+ },
+ { uri: NetUtil.newURI("http://ubuntu.org"),
+ visits: [ new VisitInfo(PlacesUtils.history.TRANSITION_DOWNLOAD) ]
+ },
+ ]
+ PlacesUtils.asyncHistory.updatePlaces(places, {
+ handleResult: function () {},
+ handleError: function () {
+ ok(false, "gHistory.updatePlaces() failed");
+ },
+ handleCompletion: function () {
+ // Make sure Downloads is present.
+ isnot(win.PlacesOrganizer._places.selectedNode, null,
+ "Downloads is present and selected");
+
+
+ // Check results.
+ let contentRoot = win.ContentArea.currentView.result.root;
+ let len = contentRoot.childCount;
+ const TEST_URIS = ["http://ubuntu.org/", "http://google.com/"];
+ for (let i = 0; i < len; i++) {
+ is(contentRoot.getChild(i).uri, TEST_URIS[i],
+ "Comparing downloads shown at index " + i);
+ }
+
+ win.close();
+ PlacesTestUtils.clearHistory().then(finish);
+ }
+ })
+ }
+
+ openLibrary(onLibraryReady, "Downloads");
+}
+
+function VisitInfo(aTransitionType)
+{
+ this.transitionType =
+ aTransitionType === undefined ?
+ PlacesUtils.history.TRANSITION_LINK : aTransitionType;
+ this.visitDate = now++ * 1000;
+}
+VisitInfo.prototype = {}
diff --git a/browser/components/places/tests/browser/browser_library_infoBox.js b/browser/components/places/tests/browser/browser_library_infoBox.js
new file mode 100644
index 000000000..17cd78f8c
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_infoBox.js
@@ -0,0 +1,197 @@
+/* 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/. */
+
+/**
+ * Test appropriate visibility of infoBoxExpanderWrapper and
+ * additionalInfoFields in infoBox section of library
+ */
+
+const TEST_URI = "http://www.mozilla.org/";
+
+var gTests = [];
+var gLibrary;
+
+// ------------------------------------------------------------------------------
+
+gTests.push({
+ desc: "Bug 430148 - Remove or hide the more/less button in details pane...",
+ run: function() {
+ var PO = gLibrary.PlacesOrganizer;
+ let ContentTree = gLibrary.ContentTree;
+ var infoBoxExpanderWrapper = getAndCheckElmtById("infoBoxExpanderWrapper");
+
+ function addVisitsCallback() {
+ // open all bookmarks node
+ PO.selectLeftPaneQuery("AllBookmarks");
+ isnot(PO._places.selectedNode, null,
+ "Correctly selected all bookmarks node.");
+ checkInfoBoxSelected(PO);
+ ok(infoBoxExpanderWrapper.hidden,
+ "Expander button is hidden for all bookmarks node.");
+ checkAddInfoFieldsCollapsed(PO);
+
+ // open history node
+ PO.selectLeftPaneQuery("History");
+ isnot(PO._places.selectedNode, null, "Correctly selected history node.");
+ checkInfoBoxSelected(PO);
+ ok(infoBoxExpanderWrapper.hidden,
+ "Expander button is hidden for history node.");
+ checkAddInfoFieldsCollapsed(PO);
+
+ // open history child node
+ var historyNode = PO._places.selectedNode.
+ QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ historyNode.containerOpen = true;
+ var childNode = historyNode.getChild(0);
+ isnot(childNode, null, "History node first child is not null.");
+ PO._places.selectNode(childNode);
+ checkInfoBoxSelected(PO);
+ ok(infoBoxExpanderWrapper.hidden,
+ "Expander button is hidden for history child node.");
+ checkAddInfoFieldsCollapsed(PO);
+
+ // open history item
+ var view = ContentTree.view.view;
+ ok(view.rowCount > 0, "History item exists.");
+ view.selection.select(0);
+ ok(infoBoxExpanderWrapper.hidden,
+ "Expander button is hidden for history item.");
+ checkAddInfoFieldsCollapsed(PO);
+
+ historyNode.containerOpen = false;
+
+ // open bookmarks menu node
+ PO.selectLeftPaneQuery("BookmarksMenu");
+ isnot(PO._places.selectedNode, null,
+ "Correctly selected bookmarks menu node.");
+ checkInfoBoxSelected(PO);
+ ok(infoBoxExpanderWrapper.hidden,
+ "Expander button is hidden for bookmarks menu node.");
+ checkAddInfoFieldsCollapsed(PO);
+
+ // open recently bookmarked node
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+ NetUtil.newURI("place:folder=BOOKMARKS_MENU" +
+ "&folder=UNFILED_BOOKMARKS" +
+ "&folder=TOOLBAR" +
+ "&queryType=" + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS +
+ "&sort=" + Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING +
+ "&maxResults=10" +
+ "&excludeQueries=1"),
+ 0, "Recent Bookmarks");
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+ NetUtil.newURI("http://mozilla.org/"),
+ 1, "Mozilla");
+ var menuNode = PO._places.selectedNode.
+ QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ menuNode.containerOpen = true;
+ childNode = menuNode.getChild(0);
+ isnot(childNode, null, "Bookmarks menu child node exists.");
+ is(childNode.title, "Recent Bookmarks",
+ "Correctly selected recently bookmarked node.");
+ PO._places.selectNode(childNode);
+ checkInfoBoxSelected(PO);
+ ok(!infoBoxExpanderWrapper.hidden,
+ "Expander button is not hidden for recently bookmarked node.");
+ checkAddInfoFieldsNotCollapsed(PO);
+
+ // open first bookmark
+ view = ContentTree.view.view;
+ ok(view.rowCount > 0, "Bookmark item exists.");
+ view.selection.select(0);
+ checkInfoBoxSelected(PO);
+ ok(!infoBoxExpanderWrapper.hidden,
+ "Expander button is not hidden for bookmark item.");
+ checkAddInfoFieldsNotCollapsed(PO);
+ checkAddInfoFields(PO, "bookmark item");
+
+ menuNode.containerOpen = false;
+
+ PlacesTestUtils.clearHistory().then(nextTest);
+ }
+ // add a visit to browser history
+ PlacesTestUtils.addVisits(
+ { uri: PlacesUtils._uri(TEST_URI), visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED }
+ ).then(addVisitsCallback);
+ }
+});
+
+function checkInfoBoxSelected(PO) {
+ is(getAndCheckElmtById("detailsDeck").selectedIndex, 1,
+ "Selected element in detailsDeck is infoBox.");
+}
+
+function checkAddInfoFieldsCollapsed(PO) {
+ PO._additionalInfoFields.forEach(function (id) {
+ ok(getAndCheckElmtById(id).collapsed,
+ "Additional info field correctly collapsed: #" + id);
+ });
+}
+
+function checkAddInfoFieldsNotCollapsed(PO) {
+ ok(PO._additionalInfoFields.some(function (id) {
+ return !getAndCheckElmtById(id).collapsed;
+ }), "Some additional info field correctly not collapsed");
+}
+
+function checkAddInfoFields(PO, nodeName) {
+ ok(true, "Checking additional info fields visibiity for node: " + nodeName);
+ var expanderButton = getAndCheckElmtById("infoBoxExpander");
+
+ // make sure additional fields are hidden by default
+ PO._additionalInfoFields.forEach(function (id) {
+ ok(getAndCheckElmtById(id).hidden,
+ "Additional info field correctly hidden by default: #" + id);
+ });
+
+ // toggle fields and make sure they are hidden/unhidden as expected
+ expanderButton.click();
+ PO._additionalInfoFields.forEach(function (id) {
+ ok(!getAndCheckElmtById(id).hidden,
+ "Additional info field correctly unhidden after toggle: #" + id);
+ });
+ expanderButton.click();
+ PO._additionalInfoFields.forEach(function (id) {
+ ok(getAndCheckElmtById(id).hidden,
+ "Additional info field correctly hidden after toggle: #" + id);
+ });
+}
+
+function getAndCheckElmtById(id) {
+ var elmt = gLibrary.document.getElementById(id);
+ isnot(elmt, null, "Correctly got element: #" + id);
+ return elmt;
+}
+
+// ------------------------------------------------------------------------------
+
+function nextTest() {
+ if (gTests.length) {
+ var test = gTests.shift();
+ ok(true, "TEST: " + test.desc);
+ dump("TEST: " + test.desc + "\n");
+ test.run();
+ }
+ else {
+ // Close Library window.
+ gLibrary.close();
+ // No need to cleanup anything, we have a correct left pane now.
+ finish();
+ }
+}
+
+function test() {
+ waitForExplicitFinish();
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils is running in chrome context");
+ ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+
+ // Open Library.
+ openLibrary(function (library) {
+ gLibrary = library;
+ gLibrary.PlacesOrganizer._places.focus();
+ nextTest(gLibrary);
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_library_left_pane_fixnames.js b/browser/components/places/tests/browser/browser_library_left_pane_fixnames.js
new file mode 100644
index 000000000..7cea38f20
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_left_pane_fixnames.js
@@ -0,0 +1,94 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Test we correctly fix broken Library left pane queries names.
+ */
+
+// Array of left pane queries objects, each one has the following properties:
+// name: query's identifier got from annotations,
+// itemId: query's itemId,
+// correctTitle: original and correct query's title.
+var leftPaneQueries = [];
+
+function onLibraryReady(organizer) {
+ // Check titles have been fixed.
+ for (var i = 0; i < leftPaneQueries.length; i++) {
+ var query = leftPaneQueries[i];
+ is(PlacesUtils.bookmarks.getItemTitle(query.itemId),
+ query.correctTitle, "Title is correct for query " + query.name);
+ if ("concreteId" in query) {
+ is(PlacesUtils.bookmarks.getItemTitle(query.concreteId),
+ query.concreteTitle, "Concrete title is correct for query " + query.name);
+ }
+ }
+
+ // Close Library window.
+ organizer.close();
+ // No need to cleanup anything, we have a correct left pane now.
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils is running in chrome context");
+ ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+ ok(PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION > 0,
+ "Left pane version in chrome context, current version is: " + PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION );
+
+ // Ensure left pane is initialized.
+ ok(PlacesUIUtils.leftPaneFolderId > 0, "left pane folder is initialized");
+
+ // Get the left pane folder.
+ var leftPaneItems = PlacesUtils.annotations
+ .getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+
+ is(leftPaneItems.length, 1, "We correctly have only 1 left pane folder");
+ // Check version.
+ var version = PlacesUtils.annotations
+ .getItemAnnotation(leftPaneItems[0],
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(version, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, "Left pane version is actual");
+
+ // Get all left pane queries.
+ var items = PlacesUtils.annotations
+ .getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_QUERY_ANNO);
+ // Get current queries names.
+ for (var i = 0; i < items.length; i++) {
+ var itemId = items[i];
+ var queryName = PlacesUtils.annotations
+ .getItemAnnotation(items[i],
+ PlacesUIUtils.ORGANIZER_QUERY_ANNO);
+ var query = { name: queryName,
+ itemId: itemId,
+ correctTitle: PlacesUtils.bookmarks.getItemTitle(itemId) }
+ switch (queryName) {
+ case "BookmarksToolbar":
+ query.concreteId = PlacesUtils.toolbarFolderId;
+ query.concreteTitle = PlacesUtils.bookmarks.getItemTitle(query.concreteId);
+ break;
+ case "BookmarksMenu":
+ query.concreteId = PlacesUtils.bookmarksMenuFolderId;
+ query.concreteTitle = PlacesUtils.bookmarks.getItemTitle(query.concreteId);
+ break;
+ case "UnfiledBookmarks":
+ query.concreteId = PlacesUtils.unfiledBookmarksFolderId;
+ query.concreteTitle = PlacesUtils.bookmarks.getItemTitle(query.concreteId);
+ break;
+ }
+ leftPaneQueries.push(query);
+ // Rename to a bad title.
+ PlacesUtils.bookmarks.setItemTitle(query.itemId, "badName");
+ if ("concreteId" in query)
+ PlacesUtils.bookmarks.setItemTitle(query.concreteId, "badName");
+ }
+
+ PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
+
+ // Open Library, this will kick-off left pane code.
+ openLibrary(onLibraryReady);
+}
diff --git a/browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js b/browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js
new file mode 100644
index 000000000..b90df120c
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function test() {
+ waitForExplicitFinish();
+ openLibrary(onLibraryReady);
+}
+
+function onLibraryReady(aLibrary) {
+ let hierarchy = [ "AllBookmarks", "BookmarksMenu" ];
+
+ let folder1 = PlacesUtils.bookmarks
+ .createFolder(PlacesUtils.bookmarksMenuFolderId,
+ "Folder 1",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ hierarchy.push(folder1);
+ let folder2 = PlacesUtils.bookmarks
+ .createFolder(folder1, "Folder 2",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ hierarchy.push(folder2);
+ let bookmark = PlacesUtils.bookmarks
+ .insertBookmark(folder2, NetUtil.newURI("http://example.com/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "Bookmark");
+
+ registerCleanupFunction(function() {
+ PlacesUtils.bookmarks.removeItem(folder1);
+ aLibrary.close();
+ });
+
+ aLibrary.PlacesOrganizer.selectLeftPaneContainerByHierarchy(hierarchy);
+
+ is(aLibrary.PlacesOrganizer._places.selectedNode.itemId, folder2,
+ "Found the expected left pane selected node");
+
+ is(aLibrary.ContentTree.view.view.nodeForTreeIndex(0).itemId, bookmark,
+ "Found the expected right pane contents");
+
+ finish();
+}
diff --git a/browser/components/places/tests/browser/browser_library_middleclick.js b/browser/components/places/tests/browser/browser_library_middleclick.js
new file mode 100644
index 000000000..894f89446
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_middleclick.js
@@ -0,0 +1,279 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+ /**
+ * Tests middle-clicking items in the Library.
+ */
+
+const ENABLE_HISTORY_PREF = "places.history.enabled";
+
+var gLibrary = null;
+var gTests = [];
+var gCurrentTest = null;
+
+// Listener for TabOpen and tabs progress.
+var gTabsListener = {
+ _loadedURIs: [],
+ _openTabsCount: 0,
+
+ handleEvent: function(aEvent) {
+ if (aEvent.type != "TabOpen")
+ return;
+
+ if (++this._openTabsCount == gCurrentTest.URIs.length) {
+ is(gBrowser.tabs.length, gCurrentTest.URIs.length + 1,
+ "We have opened " + gCurrentTest.URIs.length + " new tab(s)");
+ }
+
+ var tab = aEvent.target;
+ is(tab.ownerGlobal, window,
+ "Tab has been opened in current browser window");
+ },
+
+ onLocationChange: function(aBrowser, aWebProgress, aRequest, aLocationURI,
+ aFlags) {
+ var spec = aLocationURI.spec;
+ ok(true, spec);
+ // When a new tab is opened, location is first set to "about:blank", so
+ // we can ignore those calls.
+ // Ignore multiple notifications for the same URI too.
+ if (spec == "about:blank" || this._loadedURIs.includes(spec))
+ return;
+
+ ok(gCurrentTest.URIs.includes(spec),
+ "Opened URI found in list: " + spec);
+
+ if (gCurrentTest.URIs.includes(spec))
+ this._loadedURIs.push(spec);
+
+ if (this._loadedURIs.length == gCurrentTest.URIs.length) {
+ // We have correctly opened all URIs.
+
+ // Reset arrays.
+ this._loadedURIs.length = 0;
+
+ this._openTabsCount = 0;
+
+ executeSoon(function () {
+ // Close all tabs.
+ while (gBrowser.tabs.length > 1)
+ gBrowser.removeCurrentTab();
+
+ // Test finished. This will move to the next one.
+ waitForFocus(gCurrentTest.finish, gBrowser.ownerGlobal);
+ });
+ }
+ }
+}
+
+// ------------------------------------------------------------------------------
+// Open bookmark in a new tab.
+
+gTests.push({
+ desc: "Open bookmark in a new tab.",
+ URIs: ["about:buildconfig"],
+ _itemId: -1,
+
+ setup: function() {
+ var bs = PlacesUtils.bookmarks;
+ // Add a new unsorted bookmark.
+ this._itemId = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri(this.URIs[0]),
+ bs.DEFAULT_INDEX,
+ "Title");
+ // Select unsorted bookmarks root in the left pane.
+ gLibrary.PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ isnot(gLibrary.PlacesOrganizer._places.selectedNode, null,
+ "We correctly have selection in the Library left pane");
+ // Get our bookmark in the right pane.
+ var bookmarkNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
+ is(bookmarkNode.uri, this.URIs[0], "Found bookmark in the right pane");
+ },
+
+ finish: function() {
+ setTimeout(runNextTest, 0);
+ },
+
+ cleanup: function() {
+ PlacesUtils.bookmarks.removeItem(this._itemId);
+ }
+});
+
+// ------------------------------------------------------------------------------
+// Open a folder in tabs.
+
+gTests.push({
+ desc: "Open a folder in tabs.",
+ URIs: ["about:buildconfig", "about:"],
+ _folderId: -1,
+
+ setup: function() {
+ var bs = PlacesUtils.bookmarks;
+ // Create a new folder.
+ var folderId = bs.createFolder(bs.unfiledBookmarksFolder,
+ "Folder",
+ bs.DEFAULT_INDEX);
+ this._folderId = folderId;
+
+ // Add bookmarks in folder.
+ this.URIs.forEach(function(aURI) {
+ bs.insertBookmark(folderId,
+ PlacesUtils._uri(aURI),
+ bs.DEFAULT_INDEX,
+ "Title");
+ });
+
+ // Select unsorted bookmarks root in the left pane.
+ gLibrary.PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ isnot(gLibrary.PlacesOrganizer._places.selectedNode, null,
+ "We correctly have selection in the Library left pane");
+ // Get our bookmark in the right pane.
+ var folderNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
+ is(folderNode.title, "Folder", "Found folder in the right pane");
+ },
+
+ finish: function() {
+ setTimeout(runNextTest, 0);
+ },
+
+ cleanup: function() {
+ PlacesUtils.bookmarks.removeItem(this._folderId);
+ }
+});
+
+// ------------------------------------------------------------------------------
+// Open a query in tabs.
+
+gTests.push({
+ desc: "Open a query in tabs.",
+ URIs: ["about:buildconfig", "about:"],
+ _folderId: -1,
+ _queryId: -1,
+
+ setup: function() {
+ var bs = PlacesUtils.bookmarks;
+ // Create a new folder.
+ var folderId = bs.createFolder(bs.unfiledBookmarksFolder,
+ "Folder",
+ bs.DEFAULT_INDEX);
+ this._folderId = folderId;
+
+ // Add bookmarks in folder.
+ this.URIs.forEach(function(aURI) {
+ bs.insertBookmark(folderId,
+ PlacesUtils._uri(aURI),
+ bs.DEFAULT_INDEX,
+ "Title");
+ });
+
+ // Create a bookmarks query containing our bookmarks.
+ var hs = PlacesUtils.history;
+ var options = hs.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ var query = hs.getNewQuery();
+ // The colon included in the terms selects only about: URIs. If not included
+ // we also may get pages like about.html included in the query result.
+ query.searchTerms = "about:";
+ var queryString = hs.queriesToQueryString([query], 1, options);
+ this._queryId = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri(queryString),
+ 0, // It must be the first.
+ "Query");
+
+ // Select unsorted bookmarks root in the left pane.
+ gLibrary.PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ isnot(gLibrary.PlacesOrganizer._places.selectedNode, null,
+ "We correctly have selection in the Library left pane");
+ // Get our bookmark in the right pane.
+ var folderNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
+ is(folderNode.title, "Query", "Found query in the right pane");
+ },
+
+ finish: function() {
+ setTimeout(runNextTest, 0);
+ },
+
+ cleanup: function() {
+ PlacesUtils.bookmarks.removeItem(this._folderId);
+ PlacesUtils.bookmarks.removeItem(this._queryId);
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+function test() {
+ waitForExplicitFinish();
+ // Increase timeout, this test can be quite slow due to waitForFocus calls.
+ requestLongerTimeout(2);
+
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils in context");
+ ok(PlacesUIUtils, "PlacesUIUtils in context");
+
+ // Add tabs listeners.
+ gBrowser.tabContainer.addEventListener("TabOpen", gTabsListener, false);
+ gBrowser.addTabsProgressListener(gTabsListener);
+
+ // Temporary disable history, so we won't record pages navigation.
+ gPrefService.setBoolPref(ENABLE_HISTORY_PREF, false);
+
+ // Open Library window.
+ openLibrary(function (library) {
+ gLibrary = library;
+ // Kick off tests.
+ runNextTest();
+ });
+}
+
+function runNextTest() {
+ // Cleanup from previous test.
+ if (gCurrentTest)
+ gCurrentTest.cleanup();
+
+ if (gTests.length > 0) {
+ // Goto next test.
+ gCurrentTest = gTests.shift();
+ info("Start of test: " + gCurrentTest.desc);
+ // Test setup will set Library so that the bookmark to be opened is the
+ // first node in the content (right pane) tree.
+ gCurrentTest.setup();
+
+ // Middle click on first node in the content tree of the Library.
+ gLibrary.focus();
+ waitForFocus(function() {
+ mouseEventOnCell(gLibrary.ContentTree.view, 0, 0, { button: 1 });
+ }, gLibrary);
+ }
+ else {
+ // No more tests.
+
+ // Close Library window.
+ gLibrary.close();
+
+ // Remove tabs listeners.
+ gBrowser.tabContainer.removeEventListener("TabOpen", gTabsListener, false);
+ gBrowser.removeTabsProgressListener(gTabsListener);
+
+ // Restore history.
+ try {
+ gPrefService.clearUserPref(ENABLE_HISTORY_PREF);
+ } catch (ex) {}
+
+ finish();
+ }
+}
+
+function mouseEventOnCell(aTree, aRowIndex, aColumnIndex, aEventDetails) {
+ var selection = aTree.view.selection;
+ selection.select(aRowIndex);
+ aTree.treeBoxObject.ensureRowIsVisible(aRowIndex);
+ var column = aTree.columns[aColumnIndex];
+
+ // get cell coordinates
+ var rect = aTree.treeBoxObject.getCoordsForCellItem(aRowIndex, column, "text");
+
+ EventUtils.synthesizeMouse(aTree.body, rect.x, rect.y,
+ aEventDetails, gLibrary);
+}
diff --git a/browser/components/places/tests/browser/browser_library_openFlatContainer.js b/browser/components/places/tests/browser/browser_library_openFlatContainer.js
new file mode 100644
index 000000000..167b33031
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_openFlatContainer.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test opening a flat container in the right pane even if its parent in the
+ * left pane is closed.
+ */
+
+add_task(function* () {
+ let folder = PlacesUtils.bookmarks
+ .createFolder(PlacesUtils.unfiledBookmarksFolderId,
+ "Folder",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ let bookmark = PlacesUtils.bookmarks
+ .insertBookmark(folder, NetUtil.newURI("http://example.com/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "Bookmark");
+
+ let library = yield promiseLibrary("AllBookmarks");
+ registerCleanupFunction(function () {
+ library.close();
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ });
+
+ // Select unfiled later, to ensure it's closed.
+ library.PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ ok(!library.PlacesOrganizer._places.selectedNode.containerOpen,
+ "Unfiled container is closed");
+
+ let folderNode = library.ContentTree.view.view.nodeForTreeIndex(0);
+ is(folderNode.itemId, folder,
+ "Found the expected folder in the right pane");
+ // Select the folder node in the right pane.
+ library.ContentTree.view.selectNode(folderNode);
+
+ synthesizeClickOnSelectedTreeCell(library.ContentTree.view,
+ { clickCount: 2 });
+
+ is(library.ContentTree.view.view.nodeForTreeIndex(0).itemId, bookmark,
+ "Found the expected bookmark in the right pane");
+});
diff --git a/browser/components/places/tests/browser/browser_library_open_leak.js b/browser/components/places/tests/browser/browser_library_open_leak.js
new file mode 100644
index 000000000..f002236a9
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_open_leak.js
@@ -0,0 +1,23 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Bug 474831
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=474831
+ *
+ * Tests for leaks caused by simply opening and closing the Places Library
+ * window. Opens the Places Library window, waits for it to load, closes it,
+ * and finishes.
+ */
+
+function test() {
+ waitForExplicitFinish();
+ openLibrary(function (win) {
+ ok(true, "Library has been correctly opened");
+ win.close();
+ finish();
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_library_panel_leak.js b/browser/components/places/tests/browser/browser_library_panel_leak.js
new file mode 100644
index 000000000..643a261fb
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_panel_leak.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Bug 433231 - Places Library leaks the nsGlobalWindow when closed with a
+ * history entry selected.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=433231
+ *
+ * STRs: Open Library, select an history entry in History, close Library.
+ * ISSUE: We were adding a bookmarks observer when editing a bookmark, when
+ * selecting an history entry the panel was not un-initialized, and
+ * since an histroy entry does not have an itemId, the observer was
+ * never removed.
+ */
+
+const TEST_URI = "http://www.mozilla.org/";
+
+function test() {
+ function onLibraryReady(organizer) {
+ let contentTree = organizer.document.getElementById("placeContent");
+ isnot(contentTree, null, "Sanity check: placeContent tree should exist");
+ isnot(organizer.PlacesOrganizer, null, "Sanity check: PlacesOrganizer should exist");
+ isnot(organizer.gEditItemOverlay, null, "Sanity check: gEditItemOverlay should exist");
+
+ ok(organizer.gEditItemOverlay.initialized, "gEditItemOverlay is initialized");
+ isnot(organizer.gEditItemOverlay.itemId, -1, "Editing a bookmark");
+
+ // Select History in the left pane.
+ organizer.PlacesOrganizer.selectLeftPaneQuery('History');
+ // Select the first history entry.
+ let selection = contentTree.view.selection;
+ selection.clearSelection();
+ selection.rangedSelect(0, 0, true);
+ // Check the panel is editing the history entry.
+ is(organizer.gEditItemOverlay.itemId, -1, "Editing an history entry");
+ // Close Library window.
+ organizer.close();
+ // Clean up history.
+ PlacesTestUtils.clearHistory().then(finish);
+ }
+
+ waitForExplicitFinish();
+ // Add an history entry.
+ ok(PlacesUtils, "checking PlacesUtils, running in chrome context?");
+ PlacesTestUtils.addVisits(
+ {uri: PlacesUtils._uri(TEST_URI), visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED}
+ ).then(() => {
+ openLibrary(onLibraryReady);
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_library_search.js b/browser/components/places/tests/browser/browser_library_search.js
new file mode 100644
index 000000000..93af22363
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_search.js
@@ -0,0 +1,182 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Bug 451151
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=451151
+ *
+ * Summary:
+ * Tests frontend Places Library searching -- search, search reset, search scope
+ * consistency.
+ *
+ * Details:
+ * Each test below
+ * 1. selects a folder in the left pane and ensures that the content tree is
+ * appropriately updated,
+ * 2. performs a search and ensures that the content tree is correct for the
+ * folder and search and that the search UI is visible and appropriate to
+ * folder,
+ * 5. resets the search and ensures that the content tree is correct and that
+ * the search UI is hidden, and
+ * 6. if folder scope was clicked, searches again and ensures folder scope
+ * remains selected.
+ */
+
+const TEST_URL = "http://dummy.mozilla.org/";
+const TEST_DOWNLOAD_URL = "http://dummy.mozilla.org/dummy.pdf";
+
+var gLibrary;
+
+var testCases = [
+ function allBookmarksScope() {
+ let defScope = getDefaultScope(PlacesUIUtils.allBookmarksFolderId);
+ search(PlacesUIUtils.allBookmarksFolderId, "dummy", defScope);
+ },
+
+ function historyScope() {
+ let defScope = getDefaultScope(PlacesUIUtils.leftPaneQueries["History"]);
+ search(PlacesUIUtils.leftPaneQueries["History"], "dummy", defScope);
+ },
+
+ function downloadsScope() {
+ let defScope = getDefaultScope(PlacesUIUtils.leftPaneQueries["Downloads"]);
+ search(PlacesUIUtils.leftPaneQueries["Downloads"], "dummy", defScope);
+ },
+];
+
+/**
+ * Returns the default search scope for a given folder.
+ *
+ * @param aFolderId
+ * the item ID of a node in the left pane's tree
+ * @return the default scope when the folder is newly selected
+ */
+function getDefaultScope(aFolderId) {
+ switch (aFolderId) {
+ case PlacesUIUtils.leftPaneQueries["History"]:
+ return "scopeBarHistory"
+ case PlacesUIUtils.leftPaneQueries["Downloads"]:
+ return "scopeBarDownloads";
+ default:
+ return "scopeBarAll";
+ }
+}
+
+/**
+ * Returns the single nsINavHistoryQuery represented by a given place URI.
+ *
+ * @param aPlaceURI
+ * a URI that represents a single query
+ * @return an nsINavHistoryQuery object
+ */
+function queryStringToQuery(aPlaceURI) {
+ let queries = {};
+ PlacesUtils.history.queryStringToQueries(aPlaceURI, queries, {}, {});
+ return queries.value[0];
+}
+
+/**
+ * Resets the search by clearing the search box's text and ensures that the
+ * search scope remains as expected.
+ *
+ * @param aExpectedScopeButtonId
+ * this button should be selected after the reset
+ */
+function resetSearch(aExpectedScopeButtonId) {
+ search(null, "", aExpectedScopeButtonId);
+}
+
+/**
+ * Performs a search for a given folder and search string and ensures that the
+ * URI of the right pane's content tree is as expected for the folder and search
+ * string. Also ensures that the search scope button is as expected after the
+ * search.
+ *
+ * @param aFolderId
+ * the item ID of a node in the left pane's tree
+ * @param aSearchStr
+ * the search text; may be empty to reset the search
+ * @param aExpectedScopeButtonId
+ * after searching the selected scope button should be this
+ */
+function search(aFolderId, aSearchStr, aExpectedScopeButtonId) {
+ let doc = gLibrary.document;
+ let folderTree = doc.getElementById("placesList");
+ let contentTree = doc.getElementById("placeContent");
+
+ // First, ensure that selecting the folder in the left pane updates the
+ // content tree properly.
+ if (aFolderId) {
+ folderTree.selectItems([aFolderId]);
+ isnot(folderTree.selectedNode, null,
+ "Sanity check: left pane tree should have selection after selecting!");
+
+ // getFolders() on a History query returns an empty array, so no use
+ // comparing against aFolderId in that case.
+ if (aFolderId !== PlacesUIUtils.leftPaneQueries["History"] &&
+ aFolderId !== PlacesUIUtils.leftPaneQueries["Downloads"]) {
+ // contentTree.place should be equal to contentTree.result.root.uri,
+ // but it's not until bug 476952 is fixed.
+ let query = queryStringToQuery(contentTree.result.root.uri);
+ is(query.getFolders()[0], aFolderId,
+ "Content tree's folder should be what was selected in the left pane");
+ }
+ }
+
+ // Second, ensure that searching updates the content tree and search UI
+ // properly.
+ let searchBox = doc.getElementById("searchFilter");
+ searchBox.value = aSearchStr;
+ gLibrary.PlacesSearchBox.search(searchBox.value);
+ let query = queryStringToQuery(contentTree.result.root.uri);
+ if (aSearchStr) {
+ is(query.searchTerms, aSearchStr,
+ "Content tree's searchTerms should be text in search box");
+ }
+ else {
+ is(query.hasSearchTerms, false,
+ "Content tree's searchTerms should not exist after search reset");
+ }
+}
+
+/**
+ * test() contains window-launching boilerplate that calls this to really kick
+ * things off. Add functions to the testCases array, and this will call them.
+ */
+function onLibraryAvailable() {
+ testCases.forEach(aTest => aTest());
+
+ gLibrary.close();
+ gLibrary = null;
+
+ // Cleanup.
+ PlacesUtils.tagging.untagURI(PlacesUtils._uri(TEST_URL), ["dummyTag"]);
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ PlacesTestUtils.clearHistory().then(finish);
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ // Sanity:
+ ok(PlacesUtils, "PlacesUtils in context");
+
+ // Add visits, a bookmark and a tag.
+ PlacesTestUtils.addVisits(
+ [{ uri: PlacesUtils._uri(TEST_URL), visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED },
+ { uri: PlacesUtils._uri(TEST_DOWNLOAD_URL), visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_DOWNLOAD }]
+ ).then(() => {
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils._uri(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "dummy");
+ PlacesUtils.tagging.tagURI(PlacesUtils._uri(TEST_URL), ["dummyTag"]);
+
+ gLibrary = openLibrary(onLibraryAvailable);
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_library_views_liveupdate.js b/browser/components/places/tests/browser/browser_library_views_liveupdate.js
new file mode 100644
index 000000000..c78ed641a
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_views_liveupdate.js
@@ -0,0 +1,300 @@
+/* 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/. */
+
+/**
+ * Tests Library Left pane view for liveupdate.
+ */
+
+var gLibrary = null;
+
+function test() {
+ waitForExplicitFinish();
+ // This test takes quite some time, and timeouts frequently, so we require
+ // more time to run.
+ // See Bug 525610.
+ requestLongerTimeout(2);
+
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils in context");
+ ok(PlacesUIUtils, "PlacesUIUtils in context");
+
+ // Open Library, we will check the left pane.
+ openLibrary(function (library) {
+ gLibrary = library;
+ startTest();
+ });
+}
+
+/**
+ * Adds bookmarks observer, and executes a bunch of bookmarks operations.
+ */
+function startTest() {
+ var bs = PlacesUtils.bookmarks;
+ // Add observers.
+ bs.addObserver(bookmarksObserver, false);
+ PlacesUtils.annotations.addObserver(bookmarksObserver, false);
+ var addedBookmarks = [];
+
+ // MENU
+ ok(true, "*** Acting on menu bookmarks");
+ var id = bs.insertBookmark(bs.bookmarksMenuFolder,
+ PlacesUtils._uri("http://bm1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bm1");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(bs.bookmarksMenuFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "bm2");
+ bs.setItemTitle(id, "bm2_edited");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.bookmarksMenuFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.bookmarksMenuFolder,
+ "bmf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "bmf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://bmf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bmf1");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.bookmarksMenuFolder, 0);
+
+ // TOOLBAR
+ ok(true, "*** Acting on toolbar bookmarks");
+ bs.insertBookmark(bs.toolbarFolder,
+ PlacesUtils._uri("http://tb1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "tb1");
+ bs.setItemTitle(id, "tb1_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(bs.toolbarFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "tb2");
+ bs.setItemTitle(id, "tb2_edited");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.toolbarFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.toolbarFolder,
+ "tbf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "tbf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://tbf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bmf1");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.toolbarFolder, 0);
+
+ // UNSORTED
+ ok(true, "*** Acting on unsorted bookmarks");
+ id = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri("http://ub1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "ub1");
+ bs.setItemTitle(id, "ub1_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "ub2");
+ bs.setItemTitle(id, "ub2_edited");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.unfiledBookmarksFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.unfiledBookmarksFolder,
+ "ubf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "ubf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://ubf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "ubf1");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.unfiledBookmarksFolder, 0);
+
+ // Remove all added bookmarks.
+ addedBookmarks.forEach(function (aItem) {
+ // If we remove an item after its containing folder has been removed,
+ // this will throw, but we can ignore that.
+ try {
+ bs.removeItem(aItem);
+ } catch (ex) {}
+ });
+
+ // Remove observers.
+ bs.removeObserver(bookmarksObserver);
+ PlacesUtils.annotations.removeObserver(bookmarksObserver);
+ finishTest();
+}
+
+/**
+ * Restores browser state and calls finish.
+ */
+function finishTest() {
+ // Close Library window.
+ gLibrary.close();
+ finish();
+}
+
+/**
+ * The observer is where magic happens, for every change we do it will look for
+ * nodes positions in the affected views.
+ */
+var bookmarksObserver = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver
+ , Ci.nsIAnnotationObserver
+ ]),
+
+ // nsIAnnotationObserver
+ onItemAnnotationSet: function() {},
+ onItemAnnotationRemoved: function() {},
+ onPageAnnotationSet: function() {},
+ onPageAnnotationRemoved: function() {},
+
+ // nsINavBookmarkObserver
+ onItemAdded: function PSB_onItemAdded(aItemId, aFolderId, aIndex, aItemType,
+ aURI) {
+ var node = null;
+ var index = null;
+ [node, index] = getNodeForTreeItem(aItemId, gLibrary.PlacesOrganizer._places);
+ // Left pane should not be updated for normal bookmarks or separators.
+ switch (aItemType) {
+ case PlacesUtils.bookmarks.TYPE_BOOKMARK:
+ var uriString = aURI.spec;
+ var isQuery = uriString.substr(0, 6) == "place:";
+ if (isQuery) {
+ isnot(node, null, "Found new Places node in left pane");
+ ok(index >= 0, "Node is at index " + index);
+ break;
+ }
+ // Fallback to separator case if this is not a query.
+ case PlacesUtils.bookmarks.TYPE_SEPARATOR:
+ is(node, null, "New Places node not added in left pane");
+ break;
+ default:
+ isnot(node, null, "Found new Places node in left pane");
+ ok(index >= 0, "Node is at index " + index);
+ }
+ },
+
+ onItemRemoved: function PSB_onItemRemoved(aItemId, aFolder, aIndex) {
+ var node = null;
+ [node, ] = getNodeForTreeItem(aItemId, gLibrary.PlacesOrganizer._places);
+ is(node, null, "Places node not found in left pane");
+ },
+
+ onItemMoved: function(aItemId,
+ aOldFolderId, aOldIndex,
+ aNewFolderId, aNewIndex, aItemType) {
+ var node = null;
+ var index = null;
+ [node, index] = getNodeForTreeItem(aItemId, gLibrary.PlacesOrganizer._places);
+ // Left pane should not be updated for normal bookmarks or separators.
+ switch (aItemType) {
+ case PlacesUtils.bookmarks.TYPE_BOOKMARK:
+ var uriString = PlacesUtils.bookmarks.getBookmarkURI(aItemId).spec;
+ var isQuery = uriString.substr(0, 6) == "place:";
+ if (isQuery) {
+ isnot(node, null, "Found new Places node in left pane");
+ ok(index >= 0, "Node is at index " + index);
+ break;
+ }
+ // Fallback to separator case if this is not a query.
+ case PlacesUtils.bookmarks.TYPE_SEPARATOR:
+ is(node, null, "New Places node not added in left pane");
+ break;
+ default:
+ isnot(node, null, "Found new Places node in left pane");
+ ok(index >= 0, "Node is at index " + index);
+ }
+ },
+
+ onBeginUpdateBatch: function PSB_onBeginUpdateBatch() {},
+ onEndUpdateBatch: function PSB_onEndUpdateBatch() {},
+ onItemVisited: function() {},
+ onItemChanged: function PSB_onItemChanged(aItemId, aProperty,
+ aIsAnnotationProperty, aNewValue) {
+ if (aProperty == "title") {
+ let validator = function(aTreeRowIndex) {
+ let tree = gLibrary.PlacesOrganizer._places;
+ let cellText = tree.view.getCellText(aTreeRowIndex,
+ tree.columns.getColumnAt(0));
+ return cellText == aNewValue;
+ }
+ let [node, , valid] = getNodeForTreeItem(aItemId, gLibrary.PlacesOrganizer._places, validator);
+ if (node) // Only visible nodes.
+ ok(valid, "Title cell value has been correctly updated");
+ }
+ }
+};
+
+
+/**
+ * Get places node and index for an itemId in a tree view.
+ *
+ * @param aItemId
+ * item id of the item to search.
+ * @param aTree
+ * Tree to search in.
+ * @param aValidator [optional]
+ * function to check row validity if found. Defaults to {return true;}.
+ * @returns [node, index, valid] or [null, null, false] if not found.
+ */
+function getNodeForTreeItem(aItemId, aTree, aValidator) {
+
+ function findNode(aContainerIndex) {
+ if (aTree.view.isContainerEmpty(aContainerIndex))
+ return [null, null, false];
+
+ // The rowCount limit is just for sanity, but we will end looping when
+ // we have checked the last child of this container or we have found node.
+ for (var i = aContainerIndex + 1; i < aTree.view.rowCount; i++) {
+ var node = aTree.view.nodeForTreeIndex(i);
+
+ if (node.itemId == aItemId) {
+ // Minus one because we want relative index inside the container.
+ let valid = aValidator ? aValidator(i) : true;
+ return [node, i - aTree.view.getParentIndex(i) - 1, valid];
+ }
+
+ if (PlacesUtils.nodeIsFolder(node)) {
+ // Open container.
+ aTree.view.toggleOpenState(i);
+ // Search inside it.
+ var foundNode = findNode(i);
+ // Close container.
+ aTree.view.toggleOpenState(i);
+ // Return node if found.
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+
+ // We have finished walking this container.
+ if (!aTree.view.hasNextSibling(aContainerIndex + 1, i))
+ break;
+ }
+ return [null, null, false]
+ }
+
+ // Root node is hidden, so we need to manually walk the first level.
+ for (var i = 0; i < aTree.view.rowCount; i++) {
+ // Open container.
+ aTree.view.toggleOpenState(i);
+ // Search inside it.
+ var foundNode = findNode(i);
+ // Close container.
+ aTree.view.toggleOpenState(i);
+ // Return node if found.
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+ return [null, null, false];
+}
diff --git a/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js b/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js
new file mode 100644
index 000000000..02d564e28
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js
@@ -0,0 +1,68 @@
+/**
+ * Tests that visits across frames are correctly represented in the database.
+ */
+
+const BASE_URL = "http://mochi.test:8888/browser/browser/components/places/tests/browser";
+const PAGE_URL = BASE_URL + "/framedPage.html";
+const LEFT_URL = BASE_URL + "/frameLeft.html";
+const RIGHT_URL = BASE_URL + "/frameRight.html";
+
+add_task(function* test() {
+ // We must wait for both frames to be loaded and the visits to be registered.
+ let deferredLeftFrameVisit = PromiseUtils.defer();
+ let deferredRightFrameVisit = PromiseUtils.defer();
+
+ Services.obs.addObserver(function observe(subject) {
+ Task.spawn(function* () {
+ let url = subject.QueryInterface(Ci.nsIURI).spec;
+ if (url == LEFT_URL ) {
+ is((yield getTransitionForUrl(url)), null,
+ "Embed visits should not get a database entry.");
+ deferredLeftFrameVisit.resolve();
+ }
+ else if (url == RIGHT_URL ) {
+ is((yield getTransitionForUrl(url)),
+ PlacesUtils.history.TRANSITION_FRAMED_LINK,
+ "User activated visits should get a FRAMED_LINK transition.");
+ Services.obs.removeObserver(observe, "uri-visit-saved");
+ deferredRightFrameVisit.resolve();
+ }
+ });
+ }, "uri-visit-saved", false);
+
+ // Open a tab and wait for all the subframes to load.
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
+
+ // Wait for the left frame visit to be registered.
+ info("Waiting left frame visit");
+ yield deferredLeftFrameVisit.promise;
+
+ // Click on the link in the left frame to cause a page load in the
+ // right frame.
+ info("Clicking link");
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.frames[0].document.getElementById("clickme").click();
+ });
+
+ // Wait for the right frame visit to be registered.
+ info("Waiting right frame visit");
+ yield deferredRightFrameVisit.promise;
+
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+function* getTransitionForUrl(url) {
+ // Ensure all the transactions completed.
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(`
+ SELECT visit_type
+ FROM moz_historyvisits
+ JOIN moz_places h ON place_id = h.id
+ WHERE url_hash = hash(:url) AND url = :url`,
+ { url });
+ if (rows.length) {
+ return rows[0].getResultByName("visit_type");
+ }
+ return null;
+}
diff --git a/browser/components/places/tests/browser/browser_sidebarpanels_click.js b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
new file mode 100644
index 000000000..80ed2eb2b
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
@@ -0,0 +1,157 @@
+/* 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 test makes sure that the items in the bookmarks and history sidebar
+// panels are clickable in both LTR and RTL modes.
+
+function test() {
+ waitForExplicitFinish();
+ ignoreAllUncaughtExceptions();
+
+ const BOOKMARKS_SIDEBAR_ID = "viewBookmarksSidebar";
+ const BOOKMARKS_SIDEBAR_TREE_ID = "bookmarks-view";
+ const HISTORY_SIDEBAR_ID = "viewHistorySidebar";
+ const HISTORY_SIDEBAR_TREE_ID = "historyTree";
+ const TEST_URL = "http://mochi.test:8888/browser/browser/components/places/tests/browser/sidebarpanels_click_test_page.html";
+
+ // If a sidebar is already open, close it.
+ if (!document.getElementById("sidebar-box").hidden) {
+ info("Unexpected sidebar found - a previous test failed to cleanup correctly");
+ SidebarUI.hide();
+ }
+
+ let sidebar = document.getElementById("sidebar");
+ let tests = [];
+ let currentTest;
+
+ tests.push({
+ _itemID: null,
+ init: function(aCallback) {
+ // Add a bookmark to the Unfiled Bookmarks folder.
+ this._itemID = PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, PlacesUtils._uri(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "test"
+ );
+ aCallback();
+ },
+ prepare: function() {
+ },
+ selectNode: function(tree) {
+ tree.selectItems([this._itemID]);
+ },
+ cleanup: function(aCallback) {
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ executeSoon(aCallback);
+ },
+ sidebarName: BOOKMARKS_SIDEBAR_ID,
+ treeName: BOOKMARKS_SIDEBAR_TREE_ID,
+ desc: "Bookmarks sidebar test"
+ });
+
+ tests.push({
+ init: function(aCallback) {
+ // Add a history entry.
+ let uri = PlacesUtils._uri(TEST_URL);
+ PlacesTestUtils.addVisits({
+ uri: uri, visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED
+ }).then(aCallback);
+ },
+ prepare: function() {
+ sidebar.contentDocument.getElementById("byvisited").doCommand();
+ },
+ selectNode: function(tree) {
+ tree.selectNode(tree.view.nodeForTreeIndex(0));
+ is(tree.selectedNode.uri, TEST_URL, "The correct visit has been selected");
+ is(tree.selectedNode.itemId, -1, "The selected node is not bookmarked");
+ },
+ cleanup: function(aCallback) {
+ PlacesTestUtils.clearHistory().then(aCallback);
+ },
+ sidebarName: HISTORY_SIDEBAR_ID,
+ treeName: HISTORY_SIDEBAR_TREE_ID,
+ desc: "History sidebar test"
+ });
+
+ function testPlacesPanel(preFunc, postFunc) {
+ currentTest.init(function() {
+ SidebarUI.show(currentTest.sidebarName);
+ });
+
+ sidebar.addEventListener("load", function() {
+ sidebar.removeEventListener("load", arguments.callee, true);
+ executeSoon(function() {
+ currentTest.prepare();
+
+ if (preFunc)
+ preFunc();
+
+ function observer(aSubject, aTopic, aData) {
+ info("alert dialog observed as expected");
+ Services.obs.removeObserver(observer, "common-dialog-loaded");
+ Services.obs.removeObserver(observer, "tabmodal-dialog-loaded");
+
+ aSubject.Dialog.ui.button0.click();
+
+ executeSoon(function () {
+ SidebarUI.hide();
+ currentTest.cleanup(postFunc);
+ });
+ }
+ Services.obs.addObserver(observer, "common-dialog-loaded", false);
+ Services.obs.addObserver(observer, "tabmodal-dialog-loaded", false);
+
+ let tree = sidebar.contentDocument.getElementById(currentTest.treeName);
+
+ // Select the inserted places item.
+ currentTest.selectNode(tree);
+
+ synthesizeClickOnSelectedTreeCell(tree);
+ // Now, wait for the observer to catch the alert dialog.
+ // If something goes wrong, the test will time out at this stage.
+ // Note that for the history sidebar, the URL itself is not opened,
+ // and Places will show the load-js-data-url-error prompt as an alert
+ // box, which means that the click actually worked, so it's good enough
+ // for the purpose of this test.
+ });
+ }, true);
+ }
+
+ function changeSidebarDirection(aDirection) {
+ sidebar.contentDocument.documentElement.style.direction = aDirection;
+ }
+
+ function runNextTest() {
+ // Remove eventual tabs created by previous sub-tests.
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeTab(gBrowser.tabContainer.lastChild);
+ }
+
+ if (tests.length == 0) {
+ finish();
+ }
+ else {
+ // Create a new tab and run the test.
+ gBrowser.selectedTab = gBrowser.addTab();
+ currentTest = tests.shift();
+ testPlacesPanel(function() {
+ changeSidebarDirection("ltr");
+ info("Running " + currentTest.desc + " in LTR mode");
+ },
+ function() {
+ testPlacesPanel(function() {
+ // Run the test in RTL mode.
+ changeSidebarDirection("rtl");
+ info("Running " + currentTest.desc + " in RTL mode");
+ },
+ function() {
+ runNextTest();
+ });
+ });
+ }
+ }
+
+ // Ensure history is clean before starting the test.
+ PlacesTestUtils.clearHistory().then(runNextTest);
+}
diff --git a/browser/components/places/tests/browser/browser_sort_in_library.js b/browser/components/places/tests/browser/browser_sort_in_library.js
new file mode 100644
index 000000000..af9c35e59
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_sort_in_library.js
@@ -0,0 +1,249 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Tests the following bugs:
+ *
+ * Bug 443745 - View>Sort>of "alpha" sort items is default to Z>A instead of A>Z
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=443745
+ *
+ * Bug 444179 - Library>Views>Sort>Sort by Tags does nothing
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=444179
+ *
+ * Basically, fully tests sorting the placeContent tree in the Places Library
+ * window. Sorting is verified by comparing the nsINavHistoryResult returned by
+ * placeContent.result to the expected sort values.
+ */
+
+// Two properties of nsINavHistoryResult control the sort of the tree:
+// sortingMode and sortingAnnotation. sortingMode's value is one of the
+// nsINavHistoryQueryOptions.SORT_BY_* constants. sortingAnnotation is the
+// annotation used to sort for SORT_BY_ANNOTATION_* mode.
+//
+// This lookup table maps the possible values of anonid's of the treecols to
+// objects that represent the treecols' correct state after the user sorts the
+// previously unsorted tree by selecting a column from the Views > Sort menu.
+// sortingMode is constructed from the key and dir properties (i.e.,
+// SORT_BY_<key>_<dir>) and sortingAnnotation is checked against anno. anno
+// may be undefined if key is not "ANNOTATION".
+const SORT_LOOKUP_TABLE = {
+ title: { key: "TITLE", dir: "ASCENDING" },
+ tags: { key: "TAGS", dir: "ASCENDING" },
+ url: { key: "URI", dir: "ASCENDING" },
+ date: { key: "DATE", dir: "DESCENDING" },
+ visitCount: { key: "VISITCOUNT", dir: "DESCENDING" },
+ dateAdded: { key: "DATEADDED", dir: "DESCENDING" },
+ lastModified: { key: "LASTMODIFIED", dir: "DESCENDING" },
+ description: { key: "ANNOTATION",
+ dir: "ASCENDING",
+ anno: "bookmarkProperties/description" }
+};
+
+// This is the column that's sorted if one is not specified and the tree is
+// currently unsorted. Set it to a key substring in the name of one of the
+// nsINavHistoryQueryOptions.SORT_BY_* constants, e.g., "TITLE", "URI".
+// Method ViewMenu.setSortColumn in browser/components/places/content/places.js
+// determines this value.
+const DEFAULT_SORT_KEY = "TITLE";
+
+// Part of the test is checking that sorts stick, so each time we sort we need
+// to remember it.
+var prevSortDir = null;
+var prevSortKey = null;
+
+/**
+ * Ensures that the sort of aTree is aSortingMode and aSortingAnno.
+ *
+ * @param aTree
+ * the tree to check
+ * @param aSortingMode
+ * one of the Ci.nsINavHistoryQueryOptions.SORT_BY_* constants
+ * @param aSortingAnno
+ * checked only if sorting mode is one of the
+ * Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_* constants
+ */
+function checkSort(aTree, aSortingMode, aSortingAnno) {
+ // The placeContent tree's sort is determined by the nsINavHistoryResult it
+ // stores. Get it and check that the sort is what the caller expects.
+ let res = aTree.result;
+ isnot(res, null,
+ "sanity check: placeContent.result should not return null");
+
+ // Check sortingMode.
+ is(res.sortingMode, aSortingMode,
+ "column should now have sortingMode " + aSortingMode);
+
+ // Check sortingAnnotation, but only if sortingMode is ANNOTATION.
+ if ([Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING].
+ indexOf(aSortingMode) >= 0) {
+ is(res.sortingAnnotation, aSortingAnno,
+ "column should now have sorting annotation " + aSortingAnno);
+ }
+}
+
+/**
+ * Sets the sort of aTree.
+ *
+ * @param aOrganizerWin
+ * the Places window
+ * @param aTree
+ * the tree to sort
+ * @param aUnsortFirst
+ * true if the sort should be set to SORT_BY_NONE before sorting by aCol
+ * and aDir
+ * @param aShouldFail
+ * true if setSortColumn should fail on aCol or aDir
+ * @param aCol
+ * the column of aTree by which to sort
+ * @param aDir
+ * either "ascending" or "descending"
+ */
+function setSort(aOrganizerWin, aTree, aUnsortFirst, aShouldFail, aCol, aDir) {
+ if (aUnsortFirst) {
+ aOrganizerWin.ViewMenu.setSortColumn();
+ checkSort(aTree, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE, "");
+
+ // Remember the sort key and direction.
+ prevSortKey = null;
+ prevSortDir = null;
+ }
+
+ let failed = false;
+ try {
+ aOrganizerWin.ViewMenu.setSortColumn(aCol, aDir);
+
+ // Remember the sort key and direction.
+ if (!aCol && !aDir) {
+ prevSortKey = null;
+ prevSortDir = null;
+ }
+ else {
+ if (aCol)
+ prevSortKey = SORT_LOOKUP_TABLE[aCol.getAttribute("anonid")].key;
+ else if (prevSortKey === null)
+ prevSortKey = DEFAULT_SORT_KEY;
+
+ if (aDir)
+ prevSortDir = aDir.toUpperCase();
+ else if (prevSortDir === null)
+ prevSortDir = SORT_LOOKUP_TABLE[aCol.getAttribute("anonid")].dir;
+ }
+ } catch (exc) {
+ failed = true;
+ }
+
+ is(failed, !!aShouldFail,
+ "setSortColumn on column " +
+ (aCol ? aCol.getAttribute("anonid") : "(no column)") +
+ " with direction " + (aDir || "(no direction)") +
+ " and table previously " + (aUnsortFirst ? "unsorted" : "sorted") +
+ " should " + (aShouldFail ? "" : "not ") + "fail");
+}
+
+/**
+ * Tries sorting by an invalid column and sort direction.
+ *
+ * @param aOrganizerWin
+ * the Places window
+ * @param aPlaceContentTree
+ * the placeContent tree in aOrganizerWin
+ */
+function testInvalid(aOrganizerWin, aPlaceContentTree) {
+ // Invalid column should fail by throwing an exception.
+ let bogusCol = document.createElement("treecol");
+ bogusCol.setAttribute("anonid", "bogusColumn");
+ setSort(aOrganizerWin, aPlaceContentTree, true, true, bogusCol, "ascending");
+
+ // Invalid direction reverts to SORT_BY_NONE.
+ setSort(aOrganizerWin, aPlaceContentTree, false, false, null, "bogus dir");
+ checkSort(aPlaceContentTree, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE, "");
+}
+
+/**
+ * Tests sorting aPlaceContentTree by column only and then by both column
+ * and direction.
+ *
+ * @param aOrganizerWin
+ * the Places window
+ * @param aPlaceContentTree
+ * the placeContent tree in aOrganizerWin
+ * @param aUnsortFirst
+ * true if, before each sort we try, we should sort to SORT_BY_NONE
+ */
+function testSortByColAndDir(aOrganizerWin, aPlaceContentTree, aUnsortFirst) {
+ let cols = aPlaceContentTree.getElementsByTagName("treecol");
+ ok(cols.length > 0, "sanity check: placeContent should contain columns");
+
+ for (let i = 0; i < cols.length; i++) {
+ let col = cols.item(i);
+ ok(col.hasAttribute("anonid"),
+ "sanity check: column " + col.id + " should have anonid");
+
+ let colId = col.getAttribute("anonid");
+ ok(colId in SORT_LOOKUP_TABLE,
+ "sanity check: unexpected placeContent column anonid");
+
+ let sortConst =
+ "SORT_BY_" + SORT_LOOKUP_TABLE[colId].key + "_" +
+ (aUnsortFirst ? SORT_LOOKUP_TABLE[colId].dir : prevSortDir);
+ let expectedSortMode = Ci.nsINavHistoryQueryOptions[sortConst];
+ let expectedAnno = SORT_LOOKUP_TABLE[colId].anno || "";
+
+ // Test sorting by only a column.
+ setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, col);
+ checkSort(aPlaceContentTree, expectedSortMode, expectedAnno);
+
+ // Test sorting by both a column and a direction.
+ ["ascending", "descending"].forEach(function (dir) {
+ let sortConst =
+ "SORT_BY_" + SORT_LOOKUP_TABLE[colId].key + "_" + dir.toUpperCase();
+ let expectedSortMode = Ci.nsINavHistoryQueryOptions[sortConst];
+ setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, col, dir);
+ checkSort(aPlaceContentTree, expectedSortMode, expectedAnno);
+ });
+ }
+}
+
+/**
+ * Tests sorting aPlaceContentTree by direction only.
+ *
+ * @param aOrganizerWin
+ * the Places window
+ * @param aPlaceContentTree
+ * the placeContent tree in aOrganizerWin
+ * @param aUnsortFirst
+ * true if, before each sort we try, we should sort to SORT_BY_NONE
+ */
+function testSortByDir(aOrganizerWin, aPlaceContentTree, aUnsortFirst) {
+ ["ascending", "descending"].forEach(function (dir) {
+ let key = (aUnsortFirst ? DEFAULT_SORT_KEY : prevSortKey);
+ let sortConst = "SORT_BY_" + key + "_" + dir.toUpperCase();
+ let expectedSortMode = Ci.nsINavHistoryQueryOptions[sortConst];
+ setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, null, dir);
+ checkSort(aPlaceContentTree, expectedSortMode, "");
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ openLibrary(function (win) {
+ let tree = win.document.getElementById("placeContent");
+ isnot(tree, null, "sanity check: placeContent tree should exist");
+ // Run the tests.
+ testSortByColAndDir(win, tree, true);
+ testSortByColAndDir(win, tree, false);
+ testSortByDir(win, tree, true);
+ testSortByDir(win, tree, false);
+ testInvalid(win, tree);
+ // Reset the sort to SORT_BY_NONE.
+ setSort(win, tree, false, false);
+ // Close the window and finish.
+ win.close();
+ finish();
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js b/browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js
new file mode 100644
index 000000000..7a0eec22f
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js
@@ -0,0 +1,53 @@
+var bookmarksMenuButton = document.getElementById("bookmarks-menu-button");
+var BMB_menuPopup = document.getElementById("BMB_bookmarksPopup");
+var BMB_showAllBookmarks = document.getElementById("BMB_bookmarksShowAll");
+var contextMenu = document.getElementById("placesContext");
+var newBookmarkItem = document.getElementById("placesContext_new:bookmark");
+
+waitForExplicitFinish();
+add_task(function* testPopup() {
+ info("Checking popup context menu before moving the bookmarks button");
+ yield checkPopupContextMenu();
+ let pos = CustomizableUI.getPlacementOfWidget("bookmarks-menu-button").position;
+ CustomizableUI.addWidgetToArea("bookmarks-menu-button", CustomizableUI.AREA_PANEL);
+ CustomizableUI.addWidgetToArea("bookmarks-menu-button", CustomizableUI.AREA_NAVBAR, pos);
+ info("Checking popup context menu after moving the bookmarks button");
+ yield checkPopupContextMenu();
+});
+
+function* checkPopupContextMenu() {
+ let dropmarker = document.getAnonymousElementByAttribute(bookmarksMenuButton, "anonid", "dropmarker");
+ BMB_menuPopup.setAttribute("style", "transition: none;");
+ let popupShownPromise = onPopupEvent(BMB_menuPopup, "shown");
+ EventUtils.synthesizeMouseAtCenter(dropmarker, {});
+ info("Waiting for bookmarks menu to be shown.");
+ yield popupShownPromise;
+ let contextMenuShownPromise = onPopupEvent(contextMenu, "shown");
+ EventUtils.synthesizeMouseAtCenter(BMB_showAllBookmarks, {type: "contextmenu", button: 2 });
+ info("Waiting for context menu on bookmarks menu to be shown.");
+ yield contextMenuShownPromise;
+ ok(!newBookmarkItem.hasAttribute("disabled"), "New bookmark item shouldn't be disabled");
+ let contextMenuHiddenPromise = onPopupEvent(contextMenu, "hidden");
+ contextMenu.hidePopup();
+ BMB_menuPopup.removeAttribute("style");
+ info("Waiting for context menu on bookmarks menu to be hidden.");
+ yield contextMenuHiddenPromise;
+ let popupHiddenPromise = onPopupEvent(BMB_menuPopup, "hidden");
+ // Can't use synthesizeMouseAtCenter because the dropdown panel is in the way
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ info("Waiting for bookmarks menu to be hidden.");
+ yield popupHiddenPromise;
+}
+
+function onPopupEvent(popup, evt) {
+ let fullEvent = "popup" + evt;
+ let deferred = new Promise.defer();
+ let onPopupHandler = (e) => {
+ if (e.target == popup) {
+ popup.removeEventListener(fullEvent, onPopupHandler);
+ deferred.resolve();
+ }
+ };
+ popup.addEventListener(fullEvent, onPopupHandler);
+ return deferred.promise;
+}
diff --git a/browser/components/places/tests/browser/browser_views_liveupdate.js b/browser/components/places/tests/browser/browser_views_liveupdate.js
new file mode 100644
index 000000000..735d6b168
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_views_liveupdate.js
@@ -0,0 +1,475 @@
+/* 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/. */
+
+/**
+ * Tests Places views (menu, toolbar, tree) for liveupdate.
+ */
+
+var toolbar = document.getElementById("PersonalToolbar");
+var wasCollapsed = toolbar.collapsed;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ promiseSetToolbarVisibility(toolbar, true).then(openBookmarksSidebar);
+ } else {
+ openBookmarksSidebar();
+ }
+}
+
+function openBookmarksSidebar() {
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils in context");
+ ok(PlacesUIUtils, "PlacesUIUtils in context");
+
+ // Open bookmarks menu.
+ var popup = document.getElementById("bookmarksMenuPopup");
+ ok(popup, "Menu popup element exists");
+ fakeOpenPopup(popup);
+
+ // Open bookmarks sidebar.
+ var sidebar = document.getElementById("sidebar");
+ sidebar.addEventListener("load", function() {
+ sidebar.removeEventListener("load", arguments.callee, true);
+ // Need to executeSoon since the tree is initialized on sidebar load.
+ executeSoon(startTest);
+ }, true);
+ SidebarUI.show("viewBookmarksSidebar");
+}
+
+/**
+ * Simulates popup opening causing it to populate.
+ * We cannot just use menu.open, since it would not work on Mac due to native menubar.
+ */
+function fakeOpenPopup(aPopup) {
+ var popupEvent = document.createEvent("MouseEvent");
+ popupEvent.initMouseEvent("popupshowing", true, true, window, 0,
+ 0, 0, 0, 0, false, false, false, false,
+ 0, null);
+ aPopup.dispatchEvent(popupEvent);
+}
+
+/**
+ * Adds bookmarks observer, and executes a bunch of bookmarks operations.
+ */
+function startTest() {
+ var bs = PlacesUtils.bookmarks;
+ // Add observers.
+ bs.addObserver(bookmarksObserver, false);
+ PlacesUtils.annotations.addObserver(bookmarksObserver, false);
+ var addedBookmarks = [];
+
+ // MENU
+ info("*** Acting on menu bookmarks");
+ var id = bs.insertBookmark(bs.bookmarksMenuFolder,
+ PlacesUtils._uri("http://bm1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bm1");
+ bs.setItemTitle(id, "bm1_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(bs.bookmarksMenuFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "bm2");
+ bs.setItemTitle(id, "");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.bookmarksMenuFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.bookmarksMenuFolder,
+ "bmf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "bmf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://bmf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bmf1");
+ bs.setItemTitle(id, "bmf1_edited");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.bookmarksMenuFolder, 0);
+
+ // TOOLBAR
+ info("*** Acting on toolbar bookmarks");
+ id = bs.insertBookmark(bs.toolbarFolder,
+ PlacesUtils._uri("http://tb1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "tb1");
+ bs.setItemTitle(id, "tb1_edited");
+ addedBookmarks.push(id);
+ // Test live update of title.
+ bs.setItemTitle(id, "tb1_edited");
+ id = bs.insertBookmark(bs.toolbarFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "tb2");
+ bs.setItemTitle(id, "");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.toolbarFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.toolbarFolder,
+ "tbf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "tbf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://tbf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "tbf1");
+ bs.setItemTitle(id, "tbf1_edited");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.toolbarFolder, 0);
+
+ // UNSORTED
+ info("*** Acting on unsorted bookmarks");
+ id = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri("http://ub1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "ub1");
+ bs.setItemTitle(id, "ub1_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "ub2");
+ bs.setItemTitle(id, "ub2_edited");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.unfiledBookmarksFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.unfiledBookmarksFolder,
+ "ubf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "ubf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://ubf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bubf1");
+ bs.setItemTitle(id, "bubf1_edited");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.unfiledBookmarksFolder, 0);
+
+ // Remove all added bookmarks.
+ addedBookmarks.forEach(function (aItem) {
+ // If we remove an item after its containing folder has been removed,
+ // this will throw, but we can ignore that.
+ try {
+ bs.removeItem(aItem);
+ } catch (ex) {}
+ });
+
+ // Remove observers.
+ bs.removeObserver(bookmarksObserver);
+ PlacesUtils.annotations.removeObserver(bookmarksObserver);
+ finishTest();
+}
+
+/**
+ * Restores browser state and calls finish.
+ */
+function finishTest() {
+ // Close bookmarks sidebar.
+ SidebarUI.hide();
+
+ // Collapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ promiseSetToolbarVisibility(toolbar, false).then(finish);
+ } else {
+ finish();
+ }
+}
+
+/**
+ * The observer is where magic happens, for every change we do it will look for
+ * nodes positions in the affected views.
+ */
+var bookmarksObserver = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver
+ , Ci.nsIAnnotationObserver
+ ]),
+
+ // nsIAnnotationObserver
+ onItemAnnotationSet: function() {},
+ onItemAnnotationRemoved: function() {},
+ onPageAnnotationSet: function() {},
+ onPageAnnotationRemoved: function() {},
+
+ // nsINavBookmarkObserver
+ onItemAdded: function PSB_onItemAdded(aItemId, aFolderId, aIndex,
+ aItemType, aURI) {
+ var views = getViewsForFolder(aFolderId);
+ ok(views.length > 0, "Found affected views (" + views.length + "): " + views);
+
+ // Check that item has been added in the correct position.
+ for (var i = 0; i < views.length; i++) {
+ var [node, index] = searchItemInView(aItemId, views[i]);
+ isnot(node, null, "Found new Places node in " + views[i]);
+ is(index, aIndex, "Node is at index " + index);
+ }
+ },
+
+ onItemRemoved: function PSB_onItemRemoved(aItemId, aFolderId, aIndex,
+ aItemType) {
+ var views = getViewsForFolder(aFolderId);
+ ok(views.length > 0, "Found affected views (" + views.length + "): " + views);
+ // Check that item has been removed.
+ for (var i = 0; i < views.length; i++) {
+ var node = null;
+ [node, ] = searchItemInView(aItemId, views[i]);
+ is(node, null, "Places node not found in " + views[i]);
+ }
+ },
+
+ onItemMoved: function(aItemId,
+ aOldFolderId, aOldIndex,
+ aNewFolderId, aNewIndex,
+ aItemType) {
+ var views = getViewsForFolder(aNewFolderId);
+ ok(views.length > 0, "Found affected views: " + views);
+
+ // Check that item has been moved in the correct position.
+ for (var i = 0; i < views.length; i++) {
+ var node = null;
+ var index = null;
+ [node, index] = searchItemInView(aItemId, views[i]);
+ isnot(node, null, "Found new Places node in " + views[i]);
+ is(index, aNewIndex, "Node is at index " + index);
+ }
+ },
+
+ onBeginUpdateBatch: function PSB_onBeginUpdateBatch() {},
+ onEndUpdateBatch: function PSB_onEndUpdateBatch() {},
+ onItemVisited: function() {},
+
+ onItemChanged: function PSB_onItemChanged(aItemId, aProperty,
+ aIsAnnotationProperty, aNewValue,
+ aLastModified, aItemType,
+ aParentId) {
+ if (aProperty !== "title")
+ return;
+
+ var views = getViewsForFolder(aParentId);
+ ok(views.length > 0, "Found affected views (" + views.length + "): " + views);
+
+ // Check that item has been moved in the correct position.
+ let validator = function(aElementOrTreeIndex) {
+ if (typeof(aElementOrTreeIndex) == "number") {
+ var sidebar = document.getElementById("sidebar");
+ var tree = sidebar.contentDocument.getElementById("bookmarks-view");
+ let cellText = tree.view.getCellText(aElementOrTreeIndex,
+ tree.columns.getColumnAt(0));
+ if (!aNewValue)
+ return cellText == PlacesUIUtils.getBestTitle(tree.view.nodeForTreeIndex(aElementOrTreeIndex), true);
+ return cellText == aNewValue;
+ }
+ if (!aNewValue && aElementOrTreeIndex.localName != "toolbarbutton") {
+ return aElementOrTreeIndex.getAttribute("label") == PlacesUIUtils.getBestTitle(aElementOrTreeIndex._placesNode);
+ }
+ return aElementOrTreeIndex.getAttribute("label") == aNewValue;
+ };
+
+ for (var i = 0; i < views.length; i++) {
+ var [node, , valid] = searchItemInView(aItemId, views[i], validator);
+ isnot(node, null, "Found changed Places node in " + views[i]);
+ is(node.title, aNewValue, "Node has correct title: " + aNewValue);
+ ok(valid, "Node element has correct label: " + aNewValue);
+ }
+ }
+};
+
+/**
+ * Search an item id in a view.
+ *
+ * @param aItemId
+ * item id of the item to search.
+ * @param aView
+ * either "toolbar", "menu" or "sidebar"
+ * @param aValidator
+ * function to check validity of the found node element.
+ * @returns [node, index, valid] or [null, null, false] if not found.
+ */
+function searchItemInView(aItemId, aView, aValidator) {
+ switch (aView) {
+ case "toolbar":
+ return getNodeForToolbarItem(aItemId, aValidator);
+ case "menu":
+ return getNodeForMenuItem(aItemId, aValidator);
+ case "sidebar":
+ return getNodeForSidebarItem(aItemId, aValidator);
+ }
+
+ return [null, null, false];
+}
+
+/**
+ * Get places node and index for an itemId in bookmarks toolbar view.
+ *
+ * @param aItemId
+ * item id of the item to search.
+ * @returns [node, index] or [null, null] if not found.
+ */
+function getNodeForToolbarItem(aItemId, aValidator) {
+ var toolbar = document.getElementById("PlacesToolbarItems");
+
+ function findNode(aContainer) {
+ var children = aContainer.childNodes;
+ for (var i = 0, staticNodes = 0; i < children.length; i++) {
+ var child = children[i];
+
+ // Is this a Places node?
+ if (!child._placesNode || child.hasAttribute("simulated-places-node")) {
+ staticNodes++;
+ continue;
+ }
+
+ if (child._placesNode.itemId == aItemId) {
+ let valid = aValidator ? aValidator(child) : true;
+ return [child._placesNode, i - staticNodes, valid];
+ }
+
+ // Don't search in queries, they could contain our item in a
+ // different position. Search only folders
+ if (PlacesUtils.nodeIsFolder(child._placesNode)) {
+ var popup = child.lastChild;
+ popup.showPopup(popup);
+ var foundNode = findNode(popup);
+ popup.hidePopup();
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+ }
+ return [null, null];
+ }
+
+ return findNode(toolbar);
+}
+
+/**
+ * Get places node and index for an itemId in bookmarks menu view.
+ *
+ * @param aItemId
+ * item id of the item to search.
+ * @returns [node, index] or [null, null] if not found.
+ */
+function getNodeForMenuItem(aItemId, aValidator) {
+ var menu = document.getElementById("bookmarksMenu");
+
+ function findNode(aContainer) {
+ var children = aContainer.childNodes;
+ for (var i = 0, staticNodes = 0; i < children.length; i++) {
+ var child = children[i];
+
+ // Is this a Places node?
+ if (!child._placesNode || child.hasAttribute("simulated-places-node")) {
+ staticNodes++;
+ continue;
+ }
+
+ if (child._placesNode.itemId == aItemId) {
+ let valid = aValidator ? aValidator(child) : true;
+ return [child._placesNode, i - staticNodes, valid];
+ }
+
+ // Don't search in queries, they could contain our item in a
+ // different position. Search only folders
+ if (PlacesUtils.nodeIsFolder(child._placesNode)) {
+ var popup = child.lastChild;
+ fakeOpenPopup(popup);
+ var foundNode = findNode(popup);
+
+ child.open = false;
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+ }
+ return [null, null, false];
+ }
+
+ return findNode(menu.lastChild);
+}
+
+/**
+ * Get places node and index for an itemId in sidebar tree view.
+ *
+ * @param aItemId
+ * item id of the item to search.
+ * @returns [node, index] or [null, null] if not found.
+ */
+function getNodeForSidebarItem(aItemId, aValidator) {
+ var sidebar = document.getElementById("sidebar");
+ var tree = sidebar.contentDocument.getElementById("bookmarks-view");
+
+ function findNode(aContainerIndex) {
+ if (tree.view.isContainerEmpty(aContainerIndex))
+ return [null, null, false];
+
+ // The rowCount limit is just for sanity, but we will end looping when
+ // we have checked the last child of this container or we have found node.
+ for (var i = aContainerIndex + 1; i < tree.view.rowCount; i++) {
+ var node = tree.view.nodeForTreeIndex(i);
+
+ if (node.itemId == aItemId) {
+ // Minus one because we want relative index inside the container.
+ let valid = aValidator ? aValidator(i) : true;
+ return [node, i - tree.view.getParentIndex(i) - 1, valid];
+ }
+
+ if (PlacesUtils.nodeIsFolder(node)) {
+ // Open container.
+ tree.view.toggleOpenState(i);
+ // Search inside it.
+ var foundNode = findNode(i);
+ // Close container.
+ tree.view.toggleOpenState(i);
+ // Return node if found.
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+
+ // We have finished walking this container.
+ if (!tree.view.hasNextSibling(aContainerIndex + 1, i))
+ break;
+ }
+ return [null, null, false]
+ }
+
+ // Root node is hidden, so we need to manually walk the first level.
+ for (var i = 0; i < tree.view.rowCount; i++) {
+ // Open container.
+ tree.view.toggleOpenState(i);
+ // Search inside it.
+ var foundNode = findNode(i);
+ // Close container.
+ tree.view.toggleOpenState(i);
+ // Return node if found.
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+ return [null, null, false];
+}
+
+/**
+ * Get views affected by changes to a folder.
+ *
+ * @param aFolderId:
+ * item id of the folder we have changed.
+ * @returns a subset of views: ["toolbar", "menu", "sidebar"]
+ */
+function getViewsForFolder(aFolderId) {
+ var rootId = aFolderId;
+ while (!PlacesUtils.isRootItem(rootId))
+ rootId = PlacesUtils.bookmarks.getFolderIdForItem(rootId);
+
+ switch (rootId) {
+ case PlacesUtils.toolbarFolderId:
+ return ["toolbar", "sidebar"]
+ case PlacesUtils.bookmarksMenuFolderId:
+ return ["menu", "sidebar"]
+ case PlacesUtils.unfiledBookmarksFolderId:
+ return ["sidebar"]
+ }
+ return new Array();
+}
diff --git a/browser/components/places/tests/browser/frameLeft.html b/browser/components/places/tests/browser/frameLeft.html
new file mode 100644
index 000000000..5a54fe353
--- /dev/null
+++ b/browser/components/places/tests/browser/frameLeft.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Left frame</title>
+ </head>
+ <body>
+ <a id="clickme" href="frameRight.html" target="right">Open page in the right frame.</a>
+ </body>
+</html>
diff --git a/browser/components/places/tests/browser/frameRight.html b/browser/components/places/tests/browser/frameRight.html
new file mode 100644
index 000000000..226accc34
--- /dev/null
+++ b/browser/components/places/tests/browser/frameRight.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Right Frame</title>
+ </head>
+ <body>
+ This is the right frame.
+ </body>
+</html>
diff --git a/browser/components/places/tests/browser/framedPage.html b/browser/components/places/tests/browser/framedPage.html
new file mode 100644
index 000000000..d388562e6
--- /dev/null
+++ b/browser/components/places/tests/browser/framedPage.html
@@ -0,0 +1,9 @@
+<html>
+ <head>
+ <title>Framed page</title>
+ </head>
+ <frameset cols="*,*">
+ <frame name="left" src="frameLeft.html">
+ <frame name="right" src="about:mozilla">
+ </frameset>
+</html>
diff --git a/browser/components/places/tests/browser/head.js b/browser/components/places/tests/browser/head.js
new file mode 100644
index 000000000..aaf78332e
--- /dev/null
+++ b/browser/components/places/tests/browser/head.js
@@ -0,0 +1,460 @@
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+
+// We need to cache this before test runs...
+var cachedLeftPaneFolderIdGetter;
+var getter = PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
+if (!cachedLeftPaneFolderIdGetter && typeof(getter) == "function") {
+ cachedLeftPaneFolderIdGetter = getter;
+}
+
+// ...And restore it when test ends.
+registerCleanupFunction(function() {
+ let getter = PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
+ if (cachedLeftPaneFolderIdGetter && typeof(getter) != "function") {
+ PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
+ }
+});
+
+function openLibrary(callback, aLeftPaneRoot) {
+ let library = window.openDialog("chrome://browser/content/places/places.xul",
+ "", "chrome,toolbar=yes,dialog=no,resizable",
+ aLeftPaneRoot);
+ waitForFocus(function () {
+ callback(library);
+ }, library);
+
+ return library;
+}
+
+/**
+ * Returns a handle to a Library window.
+ * If one is opens returns itm otherwise it opens a new one.
+ *
+ * @param aLeftPaneRoot
+ * Hierarchy to open and select in the left pane.
+ */
+function promiseLibrary(aLeftPaneRoot) {
+ return new Promise(resolve => {
+ let library = Services.wm.getMostRecentWindow("Places:Organizer");
+ if (library && !library.closed) {
+ if (aLeftPaneRoot) {
+ library.PlacesOrganizer.selectLeftPaneContainerByHierarchy(aLeftPaneRoot);
+ }
+ resolve(library);
+ }
+ else {
+ openLibrary(resolve, aLeftPaneRoot);
+ }
+ });
+}
+
+function promiseLibraryClosed(organizer) {
+ return new Promise(resolve => {
+ // Wait for the Organizer window to actually be closed
+ organizer.addEventListener("unload", function onUnload() {
+ organizer.removeEventListener("unload", onUnload);
+ resolve();
+ });
+
+ // Close Library window.
+ organizer.close();
+ });
+}
+
+/**
+ * Waits for a clipboard operation to complete, looking for the expected type.
+ *
+ * @see waitForClipboard
+ *
+ * @param aPopulateClipboardFn
+ * Function to populate the clipboard.
+ * @param aFlavor
+ * Data flavor to expect.
+ */
+function promiseClipboard(aPopulateClipboardFn, aFlavor) {
+ return new Promise(resolve => {
+ waitForClipboard(data => !!data, aPopulateClipboardFn, resolve, aFlavor);
+ });
+}
+
+/**
+ * Waits for all pending async statements on the default connection, before
+ * proceeding with aCallback.
+ *
+ * @param aCallback
+ * Function to be called when done.
+ * @param aScope
+ * Scope for the callback.
+ * @param aArguments
+ * Arguments array for the callback.
+ *
+ * @note The result is achieved by asynchronously executing a query requiring
+ * a write lock. Since all statements on the same connection are
+ * serialized, the end of this write operation means that all writes are
+ * complete. Note that WAL makes so that writers don't block readers, but
+ * this is a problem only across different connections.
+ */
+function waitForAsyncUpdates(aCallback, aScope, aArguments)
+{
+ let scope = aScope || this;
+ let args = aArguments || [];
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let begin = db.createAsyncStatement("BEGIN EXCLUSIVE");
+ begin.executeAsync();
+ begin.finalize();
+
+ let commit = db.createAsyncStatement("COMMIT");
+ commit.executeAsync({
+ handleResult: function() {},
+ handleError: function() {},
+ handleCompletion: function(aReason)
+ {
+ aCallback.apply(scope, args);
+ }
+ });
+ commit.finalize();
+}
+
+function synthesizeClickOnSelectedTreeCell(aTree, aOptions) {
+ let tbo = aTree.treeBoxObject;
+ if (tbo.view.selection.count != 1)
+ throw new Error("The test node should be successfully selected");
+ // Get selection rowID.
+ let min = {}, max = {};
+ tbo.view.selection.getRangeAt(0, min, max);
+ let rowID = min.value;
+ tbo.ensureRowIsVisible(rowID);
+ // Calculate the click coordinates.
+ var rect = tbo.getCoordsForCellItem(rowID, aTree.columns[0], "text");
+ var x = rect.x + rect.width / 2;
+ var y = rect.y + rect.height / 2;
+ // Simulate the click.
+ EventUtils.synthesizeMouse(aTree.body, x, y, aOptions || {},
+ aTree.ownerGlobal);
+}
+
+/**
+ * Asynchronously check a url is visited.
+ *
+ * @param aURI The URI.
+ * @return {Promise}
+ * @resolves When the check has been added successfully.
+ * @rejects JavaScript exception.
+ */
+function promiseIsURIVisited(aURI) {
+ let deferred = Promise.defer();
+
+ PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) {
+ deferred.resolve(aIsVisited);
+ });
+
+ return deferred.promise;
+}
+
+function promiseBookmarksNotification(notification, conditionFn) {
+ info(`promiseBookmarksNotification: waiting for ${notification}`);
+ return new Promise((resolve) => {
+ let proxifiedObserver = new Proxy({}, {
+ get: (target, name) => {
+ if (name == "QueryInterface")
+ return XPCOMUtils.generateQI([ Ci.nsINavBookmarkObserver ]);
+ info(`promiseBookmarksNotification: got ${name} notification`);
+ if (name == notification)
+ return (...args) => {
+ if (conditionFn.apply(this, args)) {
+ PlacesUtils.bookmarks.removeObserver(proxifiedObserver, false);
+ executeSoon(resolve);
+ } else {
+ info(`promiseBookmarksNotification: skip cause condition doesn't apply to ${JSON.stringify(args)}`);
+ }
+ }
+ return () => {};
+ }
+ });
+ PlacesUtils.bookmarks.addObserver(proxifiedObserver, false);
+ });
+}
+
+function promiseHistoryNotification(notification, conditionFn) {
+ info(`Waiting for ${notification}`);
+ return new Promise((resolve) => {
+ let proxifiedObserver = new Proxy({}, {
+ get: (target, name) => {
+ if (name == "QueryInterface")
+ return XPCOMUtils.generateQI([ Ci.nsINavHistoryObserver ]);
+ if (name == notification)
+ return (...args) => {
+ if (conditionFn.apply(this, args)) {
+ PlacesUtils.history.removeObserver(proxifiedObserver, false);
+ executeSoon(resolve);
+ }
+ }
+ return () => {};
+ }
+ });
+ PlacesUtils.history.addObserver(proxifiedObserver, false);
+ });
+}
+
+/**
+ * Makes the specified toolbar visible or invisible and returns a Promise object
+ * that is resolved when the toolbar has completed any animations associated
+ * with hiding or showing the toolbar.
+ *
+ * Note that this code assumes that changes to a toolbar's visibility trigger
+ * a transition on the max-height property of the toolbar element.
+ * Changes to this styling could cause the returned Promise object to be
+ * resolved too early or not at all.
+ *
+ * @param aToolbar
+ * The toolbar to update.
+ * @param aVisible
+ * True to make the toolbar visible, false to make it hidden.
+ *
+ * @return {Promise}
+ * @resolves Any animation associated with updating the toolbar's visibility has
+ * finished.
+ * @rejects Never.
+ */
+function promiseSetToolbarVisibility(aToolbar, aVisible, aCallback) {
+ return new Promise((resolve, reject) => {
+ function listener(event) {
+ if (event.propertyName == "max-height") {
+ aToolbar.removeEventListener("transitionend", listener);
+ resolve();
+ }
+ }
+
+ let transitionProperties =
+ window.getComputedStyle(aToolbar).transitionProperty.split(", ");
+ if (isToolbarVisible(aToolbar) != aVisible &&
+ transitionProperties.some(
+ prop => prop == "max-height" || prop == "all"
+ )) {
+ // Just because max-height is a transitionable property doesn't mean
+ // a transition will be triggered, but it's more likely.
+ aToolbar.addEventListener("transitionend", listener);
+ setToolbarVisibility(aToolbar, aVisible);
+ return;
+ }
+
+ // No animation to wait for
+ setToolbarVisibility(aToolbar, aVisible);
+ resolve();
+ });
+}
+
+/**
+ * Helper function to determine if the given toolbar is in the visible
+ * state according to its autohide/collapsed attribute.
+ *
+ * @aToolbar The toolbar to query.
+ *
+ * @returns True if the relevant attribute on |aToolbar| indicates it is
+ * visible, false otherwise.
+ */
+function isToolbarVisible(aToolbar) {
+ let hidingAttribute = aToolbar.getAttribute("type") == "menubar"
+ ? "autohide"
+ : "collapsed";
+ let hidingValue = aToolbar.getAttribute(hidingAttribute).toLowerCase();
+ // Check for both collapsed="true" and collapsed="collapsed"
+ return hidingValue !== "true" && hidingValue !== hidingAttribute;
+}
+
+/**
+ * Executes a task after opening the bookmarks dialog, then cancels the dialog.
+ *
+ * @param autoCancel
+ * whether to automatically cancel the dialog at the end of the task
+ * @param openFn
+ * generator function causing the dialog to open
+ * @param task
+ * the task to execute once the dialog is open
+ */
+var withBookmarksDialog = Task.async(function* (autoCancel, openFn, taskFn) {
+ let closed = false;
+ let dialogPromise = new Promise(resolve => {
+ Services.ww.registerNotification(function winObserver(subject, topic, data) {
+ if (topic == "domwindowopened") {
+ let win = subject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function load() {
+ win.removeEventListener("load", load);
+ ok(win.location.href.startsWith("chrome://browser/content/places/bookmarkProperties"),
+ "The bookmark properties dialog is open");
+ // This is needed for the overlay.
+ waitForFocus(() => {
+ resolve(win);
+ }, win);
+ });
+ } else if (topic == "domwindowclosed") {
+ Services.ww.unregisterNotification(winObserver);
+ closed = true;
+ }
+ });
+ });
+
+ info("withBookmarksDialog: opening the dialog");
+ // The dialog might be modal and could block our events loop, so executeSoon.
+ executeSoon(openFn);
+
+ info("withBookmarksDialog: waiting for the dialog");
+ let dialogWin = yield dialogPromise;
+
+ // Ensure overlay is loaded
+ info("waiting for the overlay to be loaded");
+ yield waitForCondition(() => dialogWin.gEditItemOverlay.initialized,
+ "EditItemOverlay should be initialized");
+
+ // Check the first textbox is focused.
+ let doc = dialogWin.document;
+ let elt = doc.querySelector("textbox:not([collapsed=true])");
+ if (elt) {
+ info("waiting for focus on the first textfield");
+ yield waitForCondition(() => doc.activeElement == elt.inputField,
+ "The first non collapsed textbox should have been focused");
+ }
+
+ info("withBookmarksDialog: executing the task");
+ try {
+ yield taskFn(dialogWin);
+ } finally {
+ if (!closed) {
+ if (!autoCancel) {
+ ok(false, "The test should have closed the dialog!");
+ }
+ info("withBookmarksDialog: canceling the dialog");
+ doc.documentElement.cancelDialog();
+ }
+ }
+});
+
+/**
+ * Opens the contextual menu on the element pointed by the given selector.
+ *
+ * @param selector
+ * Valid selector syntax
+ * @return Promise
+ * Returns a Promise that resolves once the context menu has been
+ * opened.
+ */
+var openContextMenuForContentSelector = Task.async(function* (browser, selector) {
+ info("wait for the context menu");
+ let contextPromise = BrowserTestUtils.waitForEvent(document.getElementById("contentAreaContextMenu"),
+ "popupshown");
+ yield ContentTask.spawn(browser, { selector }, function* (args) {
+ let doc = content.document;
+ let elt = doc.querySelector(args.selector)
+ dump(`openContextMenuForContentSelector: found ${elt}\n`);
+
+ /* Open context menu so chrome can access the element */
+ const domWindowUtils =
+ content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ let rect = elt.getBoundingClientRect();
+ let left = rect.left + rect.width / 2;
+ let top = rect.top + rect.height / 2;
+ domWindowUtils.sendMouseEvent("contextmenu", left, top, 2,
+ 1, 0, false, 0, 0, true);
+ });
+ yield contextPromise;
+});
+
+/**
+ * Waits for a specified condition to happen.
+ *
+ * @param conditionFn
+ * a Function or a generator function, returning a boolean for whether
+ * the condition is fulfilled.
+ * @param errorMsg
+ * Error message to use if the condition has not been satisfied after a
+ * meaningful amount of tries.
+ */
+var waitForCondition = Task.async(function* (conditionFn, errorMsg) {
+ for (let tries = 0; tries < 100; ++tries) {
+ if ((yield conditionFn()))
+ return;
+ yield new Promise(resolve => {
+ if (!waitForCondition._timers) {
+ waitForCondition._timers = new Set();
+ registerCleanupFunction(() => {
+ is(waitForCondition._timers.size, 0, "All the wait timers have been removed");
+ delete waitForCondition._timers;
+ });
+ }
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ waitForCondition._timers.add(timer);
+ timer.init(() => {
+ waitForCondition._timers.delete(timer);
+ resolve();
+ }, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+ });
+ }
+ ok(false, errorMsg);
+});
+
+/**
+ * Fills a bookmarks dialog text field ensuring to cause expected edit events.
+ *
+ * @param id
+ * id of the text field
+ * @param text
+ * text to fill in
+ * @param win
+ * dialog window
+ * @param [optional] blur
+ * whether to blur at the end.
+ */
+function fillBookmarkTextField(id, text, win, blur = true) {
+ let elt = win.document.getElementById(id);
+ elt.focus();
+ elt.select();
+ for (let c of text.split("")) {
+ EventUtils.synthesizeKey(c, {}, win);
+ }
+ if (blur)
+ elt.blur();
+}
+
+/**
+ * Executes a task after opening the bookmarks or history sidebar. Takes care
+ * of closing the sidebar once done.
+ *
+ * @param type
+ * either "bookmarks" or "history".
+ * @param taskFn
+ * The task to execute once the sidebar is ready. Will get the Places
+ * tree view as input.
+ */
+var withSidebarTree = Task.async(function* (type, taskFn) {
+ let sidebar = document.getElementById("sidebar");
+ info("withSidebarTree: waiting sidebar load");
+ let sidebarLoadedPromise = new Promise(resolve => {
+ sidebar.addEventListener("load", function load() {
+ sidebar.removeEventListener("load", load, true);
+ resolve();
+ }, true);
+ });
+ let sidebarId = type == "bookmarks" ? "viewBookmarksSidebar"
+ : "viewHistorySidebar";
+ SidebarUI.show(sidebarId);
+ yield sidebarLoadedPromise;
+
+ let treeId = type == "bookmarks" ? "bookmarks-view"
+ : "historyTree";
+ let tree = sidebar.contentDocument.getElementById(treeId);
+
+ // Need to executeSoon since the tree is initialized on sidebar load.
+ info("withSidebarTree: executing the task");
+ try {
+ yield taskFn(tree);
+ } finally {
+ SidebarUI.hide();
+ }
+});
diff --git a/browser/components/places/tests/browser/keyword_form.html b/browser/components/places/tests/browser/keyword_form.html
new file mode 100644
index 000000000..a881c0d5a
--- /dev/null
+++ b/browser/components/places/tests/browser/keyword_form.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=windows-1252">
+</head>
+<body>
+ <form id="form1" method="POST" action="keyword_form.html">
+ <input type="hidden" name="accenti" value="àèìòù">
+ <input type="text" name="search">
+ </form>
+ <form id="form2" method="POST" action="keyword_form.html">
+ <input type="hidden" name="accenti" value="ùòìèà">
+ <input type="text" name="search">
+ </form>
+</body>
+</html>
diff --git a/browser/components/places/tests/browser/pageopeningwindow.html b/browser/components/places/tests/browser/pageopeningwindow.html
new file mode 100644
index 000000000..282f9c593
--- /dev/null
+++ b/browser/components/places/tests/browser/pageopeningwindow.html
@@ -0,0 +1,9 @@
+<meta charset="UTF-8">
+Hi, I was opened via a <script>document.write(location.search ?
+ "popup call from the opened window... uh oh, that shouldn't happen!" :
+ "bookmarklet, and I will open a new window myself.")</script><br>
+<script>
+ if (!location.search) {
+ open(location.href + "?donotopen=true", '_blank');
+ }
+</script>
diff --git a/browser/components/places/tests/browser/sidebarpanels_click_test_page.html b/browser/components/places/tests/browser/sidebarpanels_click_test_page.html
new file mode 100644
index 000000000..c73eaa540
--- /dev/null
+++ b/browser/components/places/tests/browser/sidebarpanels_click_test_page.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+ <title>browser_sidebarpanels_click.js test page</title>
+</head>
+<body onload="alert('test');">
+</body>
+</html>
diff --git a/browser/components/places/tests/chrome/.eslintrc.js b/browser/components/places/tests/chrome/.eslintrc.js
new file mode 100644
index 000000000..8c0f4f574
--- /dev/null
+++ b/browser/components/places/tests/chrome/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/browser/components/places/tests/chrome/chrome.ini b/browser/components/places/tests/chrome/chrome.ini
new file mode 100644
index 000000000..d7b4a55c8
--- /dev/null
+++ b/browser/components/places/tests/chrome/chrome.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+support-files = head.js
+
+[test_0_bug510634.xul]
+[test_0_multiple_left_pane.xul]
+[test_bug1163447_selectItems_through_shortcut.xul]
+[test_bug427633_no_newfolder_if_noip.xul]
+[test_bug485100-change-case-loses-tag.xul]
+[test_bug549192.xul]
+[test_bug549491.xul]
+[test_bug631374_tags_selector_scroll.xul]
+[test_editBookmarkOverlay_keywords.xul]
+[test_editBookmarkOverlay_tags_liveUpdate.xul]
+[test_selectItems_on_nested_tree.xul]
+[test_treeview_date.xul]
diff --git a/browser/components/places/tests/chrome/head.js b/browser/components/places/tests/chrome/head.js
new file mode 100644
index 000000000..26b97f6d7
--- /dev/null
+++ b/browser/components/places/tests/chrome/head.js
@@ -0,0 +1,7 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
diff --git a/browser/components/places/tests/chrome/test_0_bug510634.xul b/browser/components/places/tests/chrome/test_0_bug510634.xul
new file mode 100644
index 000000000..86e102180
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_0_bug510634.xul
@@ -0,0 +1,99 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="510634: Wrong icons on bookmarks sidebar"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ /**
+ * Bug 510634 - Wrong icons on bookmarks sidebar
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=510634
+ *
+ * Ensures that properties for special queries are set on their tree nodes,
+ * even if PlacesUIUtils.leftPaneFolderId was not initialized.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ // We need to cache and restore this getter in order to simulate
+ // Bug 510634
+ let cachedLeftPaneFolderIdGetter =
+ PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
+ // Must also cache and restore this getter as it is affected by
+ // leftPaneFolderId, from bug 564900.
+ let cachedAllBookmarksFolderIdGetter =
+ PlacesUIUtils.__lookupGetter__("allBookmarksFolderId");
+
+ let leftPaneFolderId = PlacesUIUtils.leftPaneFolderId;
+
+ // restore the getter
+ PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = "place:queryType=1&folder=" + leftPaneFolderId;
+
+ // The query-property is set on the title column for each row.
+ let titleColumn = tree.treeBoxObject.columns.getColumnAt(0);
+
+ // Open All Bookmarks
+ tree.selectItems([PlacesUIUtils.leftPaneQueries["AllBookmarks"]]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ is(PlacesUIUtils.allBookmarksFolderId, tree.selectedNode.itemId,
+ "Opened All Bookmarks");
+
+ ["History", "Downloads", "Tags", "AllBookmarks", "BookmarksToolbar",
+ "BookmarksMenu", "UnfiledBookmarks"].forEach(
+ function(aQueryName, aRow) {
+ let found = false;
+ for (let i = 0; i < tree.view.rowCount && !found; i++) {
+ rowProperties = tree.view.getCellProperties(i, titleColumn).split(" ");
+ found = rowProperties.includes("OrganizerQuery_" + aQueryName);
+ }
+ ok(found, "OrganizerQuery_" + aQueryName + " is set");
+ }
+ );
+
+ // Close the root node
+ tree.result.root.containerOpen = false;
+
+ // Restore the getters for the next test.
+ PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
+ PlacesUIUtils.__defineGetter__("allBookmarksFolderId",
+ cachedAllBookmarksFolderIdGetter);
+
+ SimpleTest.finish();
+ }
+
+ ]]>
+ </script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_0_multiple_left_pane.xul b/browser/components/places/tests/chrome/test_0_multiple_left_pane.xul
new file mode 100644
index 000000000..09a4d2054
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_0_multiple_left_pane.xul
@@ -0,0 +1,85 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<!-- Bug 466422:
+ - Check that we replace the left pane with a correct one if it gets corrupted
+ - and we end up having more than one. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Test handling of multiple left pane folders"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils is running in chrome context");
+ ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+ ok(PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION > 0,
+ "Left pane version in chrome context, " +
+ "current version is: " + PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION );
+
+ let fakeLeftPanes = [];
+ // We need 2 left pane folders to simulate a corrupt profile.
+ do {
+ let leftPaneItems = PlacesUtils.annotations.getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+
+ // Create a fake left pane folder.
+ let folder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.rootGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER
+ });
+
+ let fakeLeftPaneRoot = yield PlacesUtils.promiseItemId(folder.guid);
+ PlacesUtils.annotations.setItemAnnotation(fakeLeftPaneRoot, PlacesUIUtils.ORGANIZER_FOLDER_ANNO,
+ PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ fakeLeftPanes.push(folder.guid);
+ } while (fakeLeftPanes.length < 2);
+
+ // Initialize the left pane queries.
+ PlacesUIUtils.leftPaneFolderId;
+
+ // Check left pane.
+ ok(PlacesUIUtils.leftPaneFolderId > 0,
+ "Left pane folder correctly created");
+ let leftPaneItems = PlacesUtils.annotations.getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(leftPaneItems.length, 1,
+ "We correctly have only 1 left pane folder");
+
+ // Check that all old left pane items have been removed.
+ for (let guid of fakeLeftPanes) {
+ ok(!(yield PlacesUtils.bookmarks.fetch({guid})), "This folder should have been removed");
+ }
+ }).then(() => SimpleTest.finish());
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xul b/browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xul
new file mode 100644
index 000000000..8e3a99533
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xul
@@ -0,0 +1,89 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="1163447: selectItems in Places no longer selects items within Toolbar or Sidebar folders"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript"><![CDATA[
+
+ /**
+ * Bug 1163447: places-tree should be able to select an item within the toolbar, and
+ * unfiled bookmarks. Yet not follow recursive folder-shortcuts infinitely.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ let bmu = PlacesUtils.bookmarks;
+
+ yield bmu.insert({
+ parentGuid: bmu.toolbarGuid,
+ index: bmu.DEFAULT_INDEX,
+ type: bmu.TYPE_BOOKMARK,
+ url: "place:folder=TOOLBAR",
+ title: "shortcut to self - causing infinite recursion if not handled properly"
+ });
+
+ yield bmu.insert({
+ parentGuid: bmu.toolbarGuid,
+ index: bmu.DEFAULT_INDEX,
+ type: bmu.TYPE_BOOKMARK,
+ url: "place:folder=UNFILED_BOOKMARKS",
+ title: "shortcut to unfiled, within toolbar"
+ });
+
+ let folder = yield bmu.insert({
+ parentGuid: bmu.unfiledGuid,
+ index: bmu.DEFAULT_INDEX,
+ type: bmu.TYPE_FOLDER,
+ title: "folder within unfiled"
+ });
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = "place:folder=TOOLBAR";
+
+ // Select the folder via the selectItems(itemId) API being tested
+ let itemId = yield PlacesUtils.promiseItemId(folder.guid);
+ tree.selectItems([itemId]);
+
+ is(tree.selectedNode && tree.selectedNode.itemId, itemId, "The node was selected through the shortcut");
+
+ // Cleanup
+ yield bmu.eraseEverything();
+
+ }).catch(err => {
+ ok(false, `Uncaught error: ${err}`);
+ }).then(SimpleTest.finish);
+ }
+ ]]></script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul b/browser/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul
new file mode 100644
index 000000000..b659b2b46
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul
@@ -0,0 +1,91 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE window [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Bug 427633 - Disable creating a New Folder in the bookmarks dialogs if insertionPoint is invalid"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript"
+ src="chrome://browser/content/places/editBookmarkOverlay.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <vbox id="editBookmarkPanelContent"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ /**
+ * Bug 427633 - Disable creating a New Folder in the bookmarks dialogs if
+ * insertionPoint is invalid.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ // Add a bookmark.
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://www.example.com/",
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ title: "mozilla"
+ });
+
+ // Init panel.
+ ok(gEditItemOverlay, "gEditItemOverlay is in context");
+ let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
+ gEditItemOverlay.initPanel({ node });
+ ok(gEditItemOverlay.initialized, "gEditItemOverlay is initialized");
+
+ let tree = gEditItemOverlay._element("folderTree");
+ yield openFolderTree(tree);
+
+ tree.view.selection.clearSelection();
+ ok(document.getElementById("editBMPanel_newFolderButton").disabled,
+ "New folder button is disabled if there's no selection");
+
+ // Cleanup.
+ yield PlacesUtils.bookmarks.remove(bm.guid);
+ }).then(() => SimpleTest.finish());
+ }
+
+ function openFolderTree(tree) {
+ return new Promise(resolve => {
+ tree.addEventListener("DOMAttrModified", function onAttrModified(event) {
+ if (event.attrName == "place") {
+ tree.removeEventListener("DOMAttrModified", onAttrModified);
+ resolve();
+ }
+ });
+
+ // Open the folder tree.
+ document.getElementById("editBMPanel_foldersExpander").doCommand();
+ });
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul b/browser/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul
new file mode 100644
index 000000000..afad950cb
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul
@@ -0,0 +1,83 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE window [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="485100: Exchanging a letter of a tag name with its big/small equivalent removes tag from bookmark"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript"
+ src="chrome://browser/content/places/editBookmarkOverlay.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <vbox id="editBookmarkPanelContent"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ let testTag = "foo";
+ let testTagUpper = "Foo";
+ let testURI = Services.io.newURI("http://www.example.com/", null, null);
+
+ // Add a bookmark.
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ title: "mozilla",
+ url: testURI
+ });
+
+ // Init panel
+ ok(gEditItemOverlay, "gEditItemOverlay is in context");
+ let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
+ gEditItemOverlay.initPanel({ node });
+
+ // add a tag
+ document.getElementById("editBMPanel_tagsField").value = testTag;
+ gEditItemOverlay.onTagsFieldChange();
+
+ // test that the tag has been added in the backend
+ is(PlacesUtils.tagging.getTagsForURI(testURI)[0], testTag, "tags match");
+
+ // change the tag
+ document.getElementById("editBMPanel_tagsField").value = testTagUpper;
+ gEditItemOverlay.onTagsFieldChange();
+
+ // test that the tag has been added in the backend
+ is(PlacesUtils.tagging.getTagsForURI(testURI)[0], testTagUpper, "tags match");
+
+ // Cleanup.
+ PlacesUtils.tagging.untagURI(testURI, [testTag]);
+ yield PlacesUtils.bookmarks.remove(bm.guid);
+ }).then(() => SimpleTest.finish());
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug549192.xul b/browser/components/places/tests/chrome/test_bug549192.xul
new file mode 100644
index 000000000..4e6a89bb1
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug549192.xul
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="549192: History view not updated after deleting entry"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flatList="true"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript"><![CDATA[
+ /**
+ * Bug 874407
+ * Ensures that history views are updated properly after visits.
+ * Bug 549192
+ * Ensures that history views are updated after deleting entries.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ yield PlacesTestUtils.clearHistory();
+
+ // Add some visits.
+ let timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000);
+
+ function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+ }
+
+ let vtime = Date.now() * 1000;
+ const ttype = PlacesUtils.history.TRANSITION_TYPED;
+ let places =
+ [{ uri: Services.io.newURI("http://example.tld/", null, null),
+ visitDate: newTimeInMicroseconds(), transition: ttype },
+ { uri: Services.io.newURI("http://example2.tld/", null, null),
+ visitDate: newTimeInMicroseconds(), transition: ttype },
+ { uri: Services.io.newURI("http://example3.tld/", null, null),
+ visitDate: newTimeInMicroseconds(), transition: ttype }];
+
+ yield PlacesTestUtils.addVisits(places);
+
+ // Make a history query.
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let queryURI = PlacesUtils.history.queriesToQueryString([query], 1, opts);
+
+ // Setup the places tree contents.
+ var tree = document.getElementById("tree");
+ tree.place = queryURI;
+
+ // loop through the rows and check them.
+ let treeView = tree.view;
+ let selection = treeView.selection;
+ let rc = treeView.rowCount;
+
+ for (let i = 0; i < rc; i++) {
+ selection.select(i);
+ let node = tree.selectedNode;
+ is(node.uri, places[rc - i - 1].uri.spec,
+ "Found expected node at position " + i + ".");
+ }
+
+ is(rc, 3, "Found expected number of rows.");
+
+ // First check live-update of the view when adding visits.
+ places.forEach(place => place.visitDate = newTimeInMicroseconds());
+ yield PlacesTestUtils.addVisits(places);
+
+ for (let i = 0; i < rc; i++) {
+ selection.select(i);
+ let node = tree.selectedNode;
+ is(node.uri, places[rc - i - 1].uri.spec,
+ "Found expected node at position " + i + ".");
+ }
+
+ // Now remove the pages and verify live-update again.
+ for (let i = 0; i < rc; i++) {
+ selection.select(0);
+ let node = tree.selectedNode;
+ tree.controller.remove("Removing page");
+ ok(treeView.treeIndexForNode(node) == Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE,
+ node.uri + " removed.");
+ ok(treeView.rowCount == rc - i - 1, "Rows count decreased");
+ }
+
+ // Cleanup.
+ yield PlacesTestUtils.clearHistory();
+ }).then(() => SimpleTest.finish());
+ }
+ ]]></script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug549491.xul b/browser/components/places/tests/chrome/test_bug549491.xul
new file mode 100644
index 000000000..5ec7a765a
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug549491.xul
@@ -0,0 +1,78 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="549491: 'The root node is never visible' exception when details of the root node are modified "
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flatList="true"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Date" anonid="date" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript"><![CDATA[
+ /**
+ * Bug 549491
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=549491
+ *
+ * Ensures that changing the details of places tree's root-node doesn't
+ * throw.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ yield PlacesTestUtils.clearHistory();
+
+ yield PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://example.tld/", null, null),
+ transition: PlacesUtils.history.TRANSITION_TYPED
+ });
+
+ // Make a history query.
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ let queryURI = PlacesUtils.history.queriesToQueryString([query], 1, opts);
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = queryURI;
+
+ let rootNode = tree.result.root;
+ let obs = tree.view.QueryInterface(Ci.nsINavHistoryResultObserver);
+ obs.nodeHistoryDetailsChanged(rootNode, rootNode.time, rootNode.accessCount);
+ obs.nodeTitleChanged(rootNode, rootNode.title);
+ ok(true, "No exceptions thrown");
+
+ // Cleanup.
+ yield PlacesTestUtils.clearHistory();
+ }).then(SimpleTest.finish);
+ }
+ ]]></script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug631374_tags_selector_scroll.xul b/browser/components/places/tests/chrome/test_bug631374_tags_selector_scroll.xul
new file mode 100644
index 000000000..b1d73017f
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug631374_tags_selector_scroll.xul
@@ -0,0 +1,170 @@
+<?xml version="1.0"?>
+
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE window [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Bug 631374 - Editing tags in the selector scrolls up the listbox"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript"
+ src="chrome://browser/content/places/editBookmarkOverlay.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <vbox id="editBookmarkPanelContent"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ /**
+ * This test checks that editing tags doesn't scroll the tags selector
+ * listbox to wrong positions.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ let bs = PlacesUtils.bookmarks;
+
+ let tags = ["a", "b", "c", "d", "e", "f", "g",
+ "h", "i", "l", "m", "n", "o", "p"];
+
+ // Add a bookmark and tag it.
+ let uri1 = Services.io.newURI("http://www1.mozilla.org/", null, null);
+ let bm1 = yield bs.insert({
+ parentGuid: bs.toolbarGuid,
+ index: bs.DEFAULT_INDEX,
+ type: bs.TYPE_BOOKMARK,
+ title: "mozilla",
+ url: uri1.spec
+ });
+ PlacesUtils.tagging.tagURI(uri1, tags);
+
+ // Add a second bookmark so that tags won't disappear when unchecked.
+ let uri2 = Services.io.newURI("http://www2.mozilla.org/", null, null);
+ let bm2 = yield bs.insert({
+ parentGuid: bs.toolbarGuid,
+ index: bs.DEFAULT_INDEX,
+ type: bs.TYPE_BOOKMARK,
+ title: "mozilla",
+ url: uri2.spec
+ });
+ PlacesUtils.tagging.tagURI(uri2, tags);
+
+ // Init panel.
+ ok(gEditItemOverlay, "gEditItemOverlay is in context");
+ let node1 = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm1);
+ gEditItemOverlay.initPanel({ node: node1 });
+ ok(gEditItemOverlay.initialized, "gEditItemOverlay is initialized");
+
+ yield openTagSelector();
+ let tagsSelector = document.getElementById("editBMPanel_tagsSelector");
+
+ // Go by two so there is some untouched tag in the middle.
+ for (let i = 8; i < tags.length; i += 2) {
+ tagsSelector.selectedIndex = i;
+ let listItem = tagsSelector.selectedItem;
+ isnot(listItem, null, "Valid listItem found");
+
+ tagsSelector.ensureElementIsVisible(listItem);
+ let visibleIndex = tagsSelector.getIndexOfFirstVisibleRow();
+
+ ok(listItem.checked, "Item is checked " + i);
+ let selectedTag = listItem.label;
+
+ // Uncheck the tag.
+ listItem.checked = false;
+ is(visibleIndex, tagsSelector.getIndexOfFirstVisibleRow(),
+ "Scroll position did not change");
+
+ // The listbox is rebuilt, so we have to get the new element.
+ let newItem = tagsSelector.selectedItem;
+ isnot(newItem, null, "Valid new listItem found");
+ ok(!newItem.checked, "New listItem is unchecked " + i);
+ is(newItem.label, selectedTag, "Correct tag is still selected");
+
+ // Check the tag.
+ newItem.checked = true;
+ is(visibleIndex, tagsSelector.getIndexOfFirstVisibleRow(),
+ "Scroll position did not change");
+ }
+
+ // Remove the second bookmark, then nuke some of the tags.
+ yield bs.remove(bm2.guid);
+
+ // Doing this backwords tests more interesting paths.
+ for (let i = tags.length - 1; i >= 0 ; i -= 2) {
+ tagsSelector.selectedIndex = i;
+ let listItem = tagsSelector.selectedItem;
+ isnot(listItem, null, "Valid listItem found");
+
+ tagsSelector.ensureElementIsVisible(listItem);
+ let firstVisibleTag = tags[tagsSelector.getIndexOfFirstVisibleRow()];
+
+ ok(listItem.checked, "Item is checked " + i);
+ let selectedTag = listItem.label;
+
+ // Uncheck the tag.
+ listItem.checked = false;
+
+ // Ensure the first visible tag is still visible in the list.
+ let firstVisibleIndex = tagsSelector.getIndexOfFirstVisibleRow();
+ let lastVisibleIndex = firstVisibleIndex + tagsSelector.getNumberOfVisibleRows() -1;
+ let expectedTagIndex = tags.indexOf(firstVisibleTag);
+ ok(expectedTagIndex >= firstVisibleIndex &&
+ expectedTagIndex <= lastVisibleIndex,
+ "Scroll position is correct");
+
+ // The listbox is rebuilt, so we have to get the new element.
+ let newItem = tagsSelector.selectedItem;
+ isnot(newItem, null, "Valid new listItem found");
+ ok(newItem.checked, "New listItem is checked " + i);
+ is(tagsSelector.selectedItem.label,
+ tags[Math.min(i + 1, tags.length - 2)],
+ "The next tag is now selected");
+ }
+
+ // Cleanup.
+ yield bs.remove(bm1.guid);
+ }).then(SimpleTest.finish).catch(alert);
+ }
+
+ function openTagSelector() {
+ // Wait for the tags selector to be open.
+ let promise = new Promise(resolve => {
+ let row = document.getElementById("editBMPanel_tagsSelectorRow");
+ row.addEventListener("DOMAttrModified", function onAttrModified() {
+ row.removeEventListener("DOMAttrModified", onAttrModified);
+ resolve();
+ });
+ });
+
+ // Open the tags selector.
+ document.getElementById("editBMPanel_tagsSelectorExpander").doCommand();
+
+ return promise;
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/browser/components/places/tests/chrome/test_editBookmarkOverlay_keywords.xul b/browser/components/places/tests/chrome/test_editBookmarkOverlay_keywords.xul
new file mode 100644
index 000000000..f553d018b
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_editBookmarkOverlay_keywords.xul
@@ -0,0 +1,99 @@
+<?xml version="1.0"?>
+
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE window [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Bug 1343256 - Bookmark keywords disappear from one bookmark when adding a keyword to another bookmark"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/places/editBookmarkOverlay.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <vbox id="editBookmarkPanelContent"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+ Task.spawn(test.bind(this))
+ .catch(ex => ok(false, ex))
+ .then(() => PlacesUtils.bookmarks.eraseEverything())
+ .then(SimpleTest.finish);
+ }
+
+ function promiseOnItemChanged() {
+ return new Promise(resolve => {
+ PlacesUtils.bookmarks.addObserver({
+ onBeginUpdateBatch() {},
+ onEndUpdateBatch() {},
+ onItemAdded() {},
+ onItemRemoved() {},
+ onItemVisited() {},
+ onItemMoved() {},
+ onItemChanged(id, property, isAnno, value) {
+ PlacesUtils.bookmarks.removeObserver(this);
+ resolve({ property, value });
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
+ }, false);
+ });
+ }
+
+ function* test() {
+ ok(gEditItemOverlay, "Sanity check: gEditItemOverlay is in context");
+ let keywordField = document.getElementById("editBMPanel_keywordField");
+
+ for (let i = 0; i < 2; ++i) {
+ let bm = yield PlacesUtils.bookmarks.insert({
+ url: `http://www.test${i}.me/`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ info(`Init panel on bookmark #${i+1}`);
+ let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
+ gEditItemOverlay.initPanel({ node });
+ is(document.getElementById("editBMPanel_keywordField").value, "",
+ "The keyword field should be empty");
+ info("Add a keyword to the bookmark");
+ let promise = promiseOnItemChanged();
+ keywordField.focus();
+ keywordField.value = "kw";
+ synthesizeKey(i.toString(), {});
+ synthesizeKey("VK_RETURN", {});
+ keywordField.blur();
+ let {property, value} = yield promise;
+ is(property, "keyword", "The keyword should have been changed");
+ is(value, `kw${i}`, "The new keyword value is correct");
+ }
+
+ for (let i = 0; i < 2; ++i) {
+ let entry = yield PlacesUtils.keywords.fetch({ url: `http://www.test${i}.me/` });
+ is(entry.keyword, `kw${i}`, `The keyword for http://www.test${i}.me/ is correct`);
+ }
+ };
+ ]]>
+ </script>
+
+</window>
diff --git a/browser/components/places/tests/chrome/test_editBookmarkOverlay_tags_liveUpdate.xul b/browser/components/places/tests/chrome/test_editBookmarkOverlay_tags_liveUpdate.xul
new file mode 100644
index 000000000..1b1cc6473
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_editBookmarkOverlay_tags_liveUpdate.xul
@@ -0,0 +1,204 @@
+<?xml version="1.0"?>
+
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE window [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="485100: Exchanging a letter of a tag name with its big/small equivalent removes tag from bookmark"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript"
+ src="chrome://browser/content/places/editBookmarkOverlay.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <vbox id="editBookmarkPanelContent"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function checkTagsSelector(aAvailableTags, aCheckedTags) {
+ is(PlacesUtils.tagging.allTags.length, aAvailableTags.length,
+ "tagging service is in sync.");
+ let tagsSelector = document.getElementById("editBMPanel_tagsSelector");
+ let children = tagsSelector.childNodes;
+ is(children.length, aAvailableTags.length,
+ "Found expected number of tags in the tags selector");
+
+ Array.prototype.forEach.call(children, function (aChild) {
+ let tag = aChild.getAttribute("label");
+ ok(true, "Found tag '" + tag + "' in the selector");
+ ok(aAvailableTags.includes(tag), "Found expected tag");
+ let checked = aChild.getAttribute("checked") == "true";
+ is(checked, aCheckedTags.includes(tag),
+ "Tag is correctly marked");
+ });
+ }
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ const TEST_URI = Services.io.newURI("http://www.test.me/", null, null);
+ const TEST_URI2 = Services.io.newURI("http://www.test.again.me/", null, null);
+ const TEST_TAG = "test-tag";
+
+ ok(gEditItemOverlay, "Sanity check: gEditItemOverlay is in context");
+
+ // Open the tags selector.
+ document.getElementById("editBMPanel_tagsSelectorRow").collapsed = false;
+
+ // Add a bookmark.
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: TEST_URI.spec,
+ title: "test.me"
+ });
+
+ // Init panel.
+ let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
+ gEditItemOverlay.initPanel({ node });
+
+ // Add a tag.
+ PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]);
+
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], TEST_TAG,
+ "Correctly added tag to a single bookmark");
+ is(document.getElementById("editBMPanel_tagsField").value, TEST_TAG,
+ "Editing a single bookmark shows the added tag");
+ checkTagsSelector([TEST_TAG], [TEST_TAG]);
+
+ // Remove tag.
+ PlacesUtils.tagging.untagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], undefined,
+ "The tag has been removed");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing a single bookmark should not show any tag");
+ checkTagsSelector([], []);
+
+ // Add a second bookmark.
+ let bm2 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ title: "test.again.me",
+ url: TEST_URI2.spec
+ });
+
+ // Init panel with multiple uris.
+ gEditItemOverlay.initPanel({ uris: [TEST_URI, TEST_URI2] });
+
+ // Add a tag to the first uri.
+ PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], TEST_TAG,
+ "Correctly added a tag to the first bookmark.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing multiple bookmarks without matching tags should not show any tag.");
+ checkTagsSelector([TEST_TAG], []);
+
+ // Add a tag to the second uri.
+ PlacesUtils.tagging.tagURI(TEST_URI2, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI2)[0], TEST_TAG,
+ "Correctly added a tag to the second bookmark.");
+ is(document.getElementById("editBMPanel_tagsField").value, TEST_TAG,
+ "Editing multiple bookmarks should show matching tags.");
+ checkTagsSelector([TEST_TAG], [TEST_TAG]);
+
+ // Remove tag from the first bookmark.
+ PlacesUtils.tagging.untagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], undefined,
+ "Correctly removed tag from the first bookmark.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing multiple bookmarks without matching tags should not show any tag.");
+ checkTagsSelector([TEST_TAG], []);
+
+ // Remove tag from the second bookmark.
+ PlacesUtils.tagging.untagURI(TEST_URI2, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI2)[0], undefined,
+ "Correctly removed tag from the second bookmark.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing multiple bookmarks without matching tags should not show any tag.");
+ checkTagsSelector([], []);
+
+ // Init panel with a nsIURI entry.
+ gEditItemOverlay.initPanel({ uris: [TEST_URI] });
+
+ // Add a tag.
+ PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], TEST_TAG,
+ "Correctly added tag to the first entry.");
+ is(document.getElementById("editBMPanel_tagsField").value, TEST_TAG,
+ "Editing a single nsIURI entry shows the added tag");
+ checkTagsSelector([TEST_TAG], [TEST_TAG]);
+
+ // Remove tag.
+ PlacesUtils.tagging.untagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], undefined,
+ "Correctly removed tag from the nsIURI entry.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing a single nsIURI entry should not show any tag");
+ checkTagsSelector([], []);
+
+ // Init panel with multiple nsIURI entries.
+ gEditItemOverlay.initPanel({ uris: [TEST_URI, TEST_URI2] });
+
+ // Add a tag to the first entry.
+ PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], TEST_TAG,
+ "Tag correctly added.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing multiple nsIURIs without matching tags should not show any tag.");
+ checkTagsSelector([TEST_TAG], []);
+
+ // Add a tag to the second entry.
+ PlacesUtils.tagging.tagURI(TEST_URI2, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI2)[0], TEST_TAG,
+ "Tag correctly added.");
+ is(document.getElementById("editBMPanel_tagsField").value, TEST_TAG,
+ "Editing multiple nsIURIs should show matching tags");
+ checkTagsSelector([TEST_TAG], [TEST_TAG]);
+
+ // Remove tag from the first entry.
+ PlacesUtils.tagging.untagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], undefined,
+ "Correctly removed tag from the first entry.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing multiple nsIURIs without matching tags should not show any tag.");
+ checkTagsSelector([TEST_TAG], []);
+
+ // Remove tag from the second entry.
+ PlacesUtils.tagging.untagURI(TEST_URI2, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI2)[0], undefined,
+ "Correctly removed tag from the second entry.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing multiple nsIURIs without matching tags should not show any tag.");
+ checkTagsSelector([], []);
+
+ // Cleanup.
+ yield PlacesUtils.bookmarks.remove(bm.guid);
+ yield PlacesUtils.bookmarks.remove(bm2.guid);
+ }).then(SimpleTest.finish);
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xul b/browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xul
new file mode 100644
index 000000000..032c7a258
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xul
@@ -0,0 +1,86 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="549192: History view not updated after deleting entry"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript"><![CDATA[
+ /**
+ * Ensure that selectItems doesn't recurse infinitely in nested trees.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "place:folder=UNFILED_BOOKMARKS",
+ title: "shortcut"
+ });
+
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "place:folder=UNFILED_BOOKMARKS&maxResults=10",
+ title: "query"
+ });
+
+ let folder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "folder"
+ });
+
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://www.mozilla.org/",
+ title: "bookmark"
+ });
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = "place:folder=UNFILED_BOOKMARKS";
+
+ // Select the last bookmark.
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ tree.selectItems([itemId]);
+ is (tree.selectedNode.itemId, itemId, "The right node was selected");
+ }).then(SimpleTest.finish);
+ }
+ ]]></script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_treeview_date.xul b/browser/components/places/tests/chrome/test_treeview_date.xul
new file mode 100644
index 000000000..559232611
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_treeview_date.xul
@@ -0,0 +1,159 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="435322: Places tree view's formatting"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flatList="true"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Tags" id="tags" anonid="tags" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Url" id="url" anonid="url" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Visit Date" id="date" anonid="date" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Visit Count" id="visitCount" anonid="visitCount" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ /**
+ * Bug 435322
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=435322
+ *
+ * Ensures that date in places treeviews is correctly formatted.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ function uri(spec) {
+ return Services.io.newURI(spec, null, null);
+ }
+
+ Task.spawn(function* () {
+ yield PlacesTestUtils.clearHistory();
+
+ let midnight = new Date();
+ midnight.setHours(0);
+ midnight.setMinutes(0);
+ midnight.setSeconds(0);
+ midnight.setMilliseconds(0);
+
+ // Add a visit 1ms before midnight, a visit at midnight, and
+ // a visit 1ms after midnight.
+ yield PlacesTestUtils.addVisits([
+ {uri: uri("http://before.midnight.com/"),
+ visitDate: (midnight.getTime() - 1) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED},
+ {uri: uri("http://at.midnight.com/"),
+ visitDate: (midnight.getTime()) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED},
+ {uri: uri("http://after.midnight.com/"),
+ visitDate: (midnight.getTime() + 1) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED}
+ ]);
+
+ // add a bookmark to the midnight visit
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ url: "http://at.midnight.com/",
+ title: "A bookmark at midnight",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK
+ });
+
+ // Make a history query.
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ let queryURI = PlacesUtils.history.queriesToQueryString([query], 1, opts);
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = queryURI;
+
+ // loop through the rows and check formatting
+ let treeView = tree.view;
+ let rc = treeView.rowCount;
+ ok(rc >= 3, "Rows found");
+ let columns = tree.columns;
+ ok(columns.count > 0, "Columns found");
+ const locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIXULChromeRegistry)
+ .getSelectedLocale("global", true);
+ for (let r = 0; r < rc; r++) {
+ let node = treeView.nodeForTreeIndex(r);
+ ok(node, "Places node found");
+ for (let ci = 0; ci < columns.count; ci++) {
+ let c = columns.getColumnAt(ci);
+ let text = treeView.getCellText(r, c);
+ switch (c.element.getAttribute("anonid")) {
+ case "title":
+ // The title can differ, we did not set any title so we would
+ // expect null, but in such a case the view will generate a title
+ // through PlacesUIUtils.getBestTitle.
+ if (node.title)
+ is(text, node.title, "Title is correct");
+ break;
+ case "url":
+ is(text, node.uri, "Uri is correct");
+ break;
+ case "date":
+ let timeObj = new Date(node.time / 1000);
+ // Default is short date format.
+ let dtOptions = { year: 'numeric', month: 'numeric', day: 'numeric',
+ hour: 'numeric', minute: 'numeric' };
+ // For today's visits we don't show date portion.
+ if (node.uri == "http://at.midnight.com/" ||
+ node.uri == "http://after.midnight.com/") {
+ dtOptions = { hour: 'numeric', minute: 'numeric' };
+ } else if (node.uri != "http://before.midnight.com/") {
+ // Avoid to test spurious uris, due to how the test works
+ // a redirecting uri could be put in the tree while we test.
+ break;
+ }
+ let timeStr = timeObj.toLocaleString(locale, dtOptions);
+
+ is(text, timeStr, "Date format is correct");
+ break;
+ case "visitCount":
+ is(text, 1, "Visit count is correct");
+ break;
+ }
+ }
+ }
+
+ // Cleanup.
+ yield PlacesUtils.bookmarks.remove(bm.guid);
+ yield PlacesTestUtils.clearHistory();
+ }).then(SimpleTest.finish);
+ }
+ ]]>
+ </script>
+</window>
diff --git a/browser/components/places/tests/unit/.eslintrc.js b/browser/components/places/tests/unit/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/browser/components/places/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/browser/components/places/tests/unit/bookmarks.glue.html b/browser/components/places/tests/unit/bookmarks.glue.html
new file mode 100644
index 000000000..07b22e9b3
--- /dev/null
+++ b/browser/components/places/tests/unit/bookmarks.glue.html
@@ -0,0 +1,16 @@
+<!DOCTYPE NETSCAPE-Bookmark-file-1>
+<!-- This is an automatically generated file.
+ It will be read and overwritten.
+ DO NOT EDIT! -->
+<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
+<TITLE>Bookmarks</TITLE>
+<H1>Bookmarks Menu</H1>
+
+<DL><p>
+ <DT><A HREF="http://example.com/" ADD_DATE="1233157972" LAST_MODIFIED="1233157984">example</A>
+ <DT><H3 ADD_DATE="1233157910" LAST_MODIFIED="1233157972" PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Toolbar</H3>
+<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar
+ <DL><p>
+ <DT><A HREF="http://example.com/" ADD_DATE="1233157972" LAST_MODIFIED="1233157984">example</A>
+ </DL><p>
+</DL><p>
diff --git a/browser/components/places/tests/unit/bookmarks.glue.json b/browser/components/places/tests/unit/bookmarks.glue.json
new file mode 100644
index 000000000..95900e176
--- /dev/null
+++ b/browser/components/places/tests/unit/bookmarks.glue.json
@@ -0,0 +1 @@
+{"title":"","id":1,"dateAdded":1233157910552624,"lastModified":1233157955206833,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"title":"Bookmarks Menu","id":2,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157993171424,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"title":"examplejson","id":27,"parent":2,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":1,"title":"Bookmarks Toolbar","id":3,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157972101126,"annos":[{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar"}],"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"title":"examplejson","id":26,"parent":3,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":2,"title":"Tags","id":4,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157910582667,"type":"text/x-moz-place-container","root":"tagsFolder","children":[]},{"index":3,"title":"Other Bookmarks","id":5,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157911033315,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[]}]}
diff --git a/browser/components/places/tests/unit/corruptDB.sqlite b/browser/components/places/tests/unit/corruptDB.sqlite
new file mode 100644
index 000000000..b234246ca
--- /dev/null
+++ b/browser/components/places/tests/unit/corruptDB.sqlite
Binary files differ
diff --git a/browser/components/places/tests/unit/distribution.ini b/browser/components/places/tests/unit/distribution.ini
new file mode 100644
index 000000000..93e73cb5c
--- /dev/null
+++ b/browser/components/places/tests/unit/distribution.ini
@@ -0,0 +1,27 @@
+# Distribution Configuration File
+# Bug 516444 demo
+
+[Global]
+id=516444
+version=1.0
+about=Test distribution file
+
+[BookmarksToolbar]
+item.1.title=Toolbar Link Before
+item.1.link=https://example.org/toolbar/before/
+item.1.keyword=e:t:b
+item.1.icon=https://example.org/favicon.png
+item.1.iconData=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==
+item.2.type=default
+item.3.title=Toolbar Link After
+item.3.link=https://example.org/toolbar/after/
+item.3.keyword=e:t:a
+
+[BookmarksMenu]
+item.1.title=Menu Link Before
+item.1.link=https://example.org/menu/before/
+item.1.icon=https://example.org/favicon.png
+item.1.iconData=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==
+item.2.type=default
+item.3.title=Menu Link After
+item.3.link=https://example.org/menu/after/
diff --git a/browser/components/places/tests/unit/head_bookmarks.js b/browser/components/places/tests/unit/head_bookmarks.js
new file mode 100644
index 000000000..460295f96
--- /dev/null
+++ b/browser/components/places/tests/unit/head_bookmarks.js
@@ -0,0 +1,133 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/LoadContextInfo.jsm");
+
+// Import common head.
+var commonFile = do_get_file("../../../../../toolkit/components/places/tests/head_common.js", false);
+if (commonFile) {
+ let uri = Services.io.newFileURI(commonFile);
+ Services.scriptloader.loadSubScript(uri.spec, this);
+}
+
+// Put any other stuff relative to this test folder below.
+
+XPCOMUtils.defineLazyGetter(this, "PlacesUIUtils", function() {
+ Cu.import("resource:///modules/PlacesUIUtils.jsm");
+ return PlacesUIUtils;
+});
+
+const ORGANIZER_FOLDER_ANNO = "PlacesOrganizer/OrganizerFolder";
+const ORGANIZER_QUERY_ANNO = "PlacesOrganizer/OrganizerQuery";
+
+// Needed by some test that relies on having an app registered.
+Cu.import("resource://testing-common/AppInfo.jsm", this);
+updateAppInfo({
+ name: "PlacesTest",
+ ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
+ version: "1",
+ platformVersion: "",
+});
+
+// Smart bookmarks constants.
+const SMART_BOOKMARKS_VERSION = 8;
+const SMART_BOOKMARKS_ON_TOOLBAR = 1;
+const SMART_BOOKMARKS_ON_MENU = 2; // Takes into account the additional separator.
+
+// Default bookmarks constants.
+const DEFAULT_BOOKMARKS_ON_TOOLBAR = 1;
+const DEFAULT_BOOKMARKS_ON_MENU = 1;
+
+const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
+
+function checkItemHasAnnotation(guid, name) {
+ return PlacesUtils.promiseItemId(guid).then(id => {
+ let hasAnnotation = PlacesUtils.annotations.itemHasAnnotation(id, name);
+ Assert.ok(hasAnnotation, `Expected annotation ${name}`);
+ });
+}
+
+var createCorruptDB = Task.async(function* () {
+ let dbPath = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
+ yield OS.File.remove(dbPath);
+
+ // Create a corrupt database.
+ let dir = yield OS.File.getCurrentDirectory();
+ let src = OS.Path.join(dir, "corruptDB.sqlite");
+ yield OS.File.copy(src, dbPath);
+
+ // Check there's a DB now.
+ Assert.ok((yield OS.File.exists(dbPath)), "should have a DB now");
+});
+
+/**
+ * Rebuilds smart bookmarks listening to console output to report any message or
+ * exception generated.
+ *
+ * @return {Promise}
+ * Resolved when done.
+ */
+function rebuildSmartBookmarks() {
+ let consoleListener = {
+ observe(aMsg) {
+ if (aMsg.message.startsWith("[JavaScript Warning:")) {
+ // TODO (Bug 1300416): Ignore spurious strict warnings.
+ return;
+ }
+ do_throw("Got console message: " + aMsg.message);
+ },
+ QueryInterface: XPCOMUtils.generateQI([ Ci.nsIConsoleListener ]),
+ };
+ Services.console.reset();
+ Services.console.registerListener(consoleListener);
+ do_register_cleanup(() => {
+ try {
+ Services.console.unregisterListener(consoleListener);
+ } catch (ex) { /* will likely fail */ }
+ });
+ Cc["@mozilla.org/browser/browserglue;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "browser-glue-test", "smart-bookmarks-init");
+ return promiseTopicObserved("test-smart-bookmarks-done").then(() => {
+ Services.console.unregisterListener(consoleListener);
+ });
+}
+
+const SINGLE_TRY_TIMEOUT = 100;
+const NUMBER_OF_TRIES = 30;
+
+/**
+ * Similar to waitForConditionPromise, but poll for an asynchronous value
+ * every SINGLE_TRY_TIMEOUT ms, for no more than tryCount times.
+ *
+ * @param promiseFn
+ * A function to generate a promise, which resolves to the expected
+ * asynchronous value.
+ * @param timeoutMsg
+ * The reason to reject the returned promise with.
+ * @param [optional] tryCount
+ * Maximum times to try before rejecting the returned promise with
+ * timeoutMsg, defaults to NUMBER_OF_TRIES.
+ * @return {Promise}
+ * @resolves to the asynchronous value being polled.
+ * @rejects if the asynchronous value is not available after tryCount attempts.
+ */
+var waitForResolvedPromise = Task.async(function* (promiseFn, timeoutMsg, tryCount=NUMBER_OF_TRIES) {
+ let tries = 0;
+ do {
+ try {
+ let value = yield promiseFn();
+ return value;
+ } catch (ex) {}
+ yield new Promise(resolve => do_timeout(SINGLE_TRY_TIMEOUT, resolve));
+ } while (++tries <= tryCount);
+ throw new Error(timeoutMsg);
+});
diff --git a/browser/components/places/tests/unit/test_421483.js b/browser/components/places/tests/unit/test_421483.js
new file mode 100644
index 000000000..a0d138372
--- /dev/null
+++ b/browser/components/places/tests/unit/test_421483.js
@@ -0,0 +1,103 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 SMART_BOOKMARKS_PREF = "browser.places.smartBookmarksVersion";
+
+var gluesvc = Cc["@mozilla.org/browser/browserglue;1"].
+ getService(Ci.nsIObserver);
+// Avoid default bookmarks import.
+gluesvc.observe(null, "initial-migration-will-import-default-bookmarks", "");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* smart_bookmarks_disabled() {
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1);
+ yield rebuildSmartBookmarks();
+
+ let smartBookmarkItemIds =
+ PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ Assert.equal(smartBookmarkItemIds.length, 0);
+
+ do_print("check that pref has not been bumped up");
+ Assert.equal(Services.prefs.getIntPref("browser.places.smartBookmarksVersion"), -1);
+});
+
+add_task(function* create_smart_bookmarks() {
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
+ yield rebuildSmartBookmarks();
+
+ let smartBookmarkItemIds =
+ PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ Assert.notEqual(smartBookmarkItemIds.length, 0);
+
+ do_print("check that pref has been bumped up");
+ Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+});
+
+add_task(function* remove_smart_bookmark_and_restore() {
+ let smartBookmarkItemIds =
+ PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ let smartBookmarksCount = smartBookmarkItemIds.length;
+ do_print("remove one smart bookmark and restore");
+
+ let guid = yield PlacesUtils.promiseItemGuid(smartBookmarkItemIds[0]);
+ yield PlacesUtils.bookmarks.remove(guid);
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
+
+ yield rebuildSmartBookmarks();
+ smartBookmarkItemIds =
+ PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ Assert.equal(smartBookmarkItemIds.length, smartBookmarksCount);
+
+ do_print("check that pref has been bumped up");
+ Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+});
+
+add_task(function* move_smart_bookmark_rename_and_restore() {
+ let smartBookmarkItemIds =
+ PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ let smartBookmarksCount = smartBookmarkItemIds.length;
+ do_print("smart bookmark should be restored in place");
+
+ let guid = yield PlacesUtils.promiseItemGuid(smartBookmarkItemIds[0]);
+ let bm = yield PlacesUtils.bookmarks.fetch(guid);
+ let oldTitle = bm.title;
+
+ // create a subfolder and move inside it
+ let subfolder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: bm.parentGuid,
+ title: "test",
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER
+ });
+
+ // change title and move into new subfolder
+ yield PlacesUtils.bookmarks.update({
+ guid: guid,
+ parentGuid: subfolder.guid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "new title"
+ });
+
+ // restore
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
+ yield rebuildSmartBookmarks();
+
+ smartBookmarkItemIds =
+ PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ Assert.equal(smartBookmarkItemIds.length, smartBookmarksCount);
+
+ guid = yield PlacesUtils.promiseItemGuid(smartBookmarkItemIds[0]);
+ bm = yield PlacesUtils.bookmarks.fetch(guid);
+ Assert.equal(bm.parentGuid, subfolder.guid);
+ Assert.equal(bm.title, oldTitle);
+
+ do_print("check that pref has been bumped up");
+ Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+});
diff --git a/browser/components/places/tests/unit/test_PUIU_makeTransaction.js b/browser/components/places/tests/unit/test_PUIU_makeTransaction.js
new file mode 100644
index 000000000..c0626f53b
--- /dev/null
+++ b/browser/components/places/tests/unit/test_PUIU_makeTransaction.js
@@ -0,0 +1,361 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function waitForBookmarkNotification(aNotification, aCallback, aProperty)
+{
+ PlacesUtils.bookmarks.addObserver({
+ validate: function (aMethodName, aData)
+ {
+ if (aMethodName == aNotification &&
+ (!aProperty || aProperty == aData.property)) {
+ PlacesUtils.bookmarks.removeObserver(this);
+ aCallback(aData);
+ }
+ },
+
+ // nsINavBookmarkObserver
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]),
+ onBeginUpdateBatch: function onBeginUpdateBatch() {
+ return this.validate(arguments.callee.name, arguments);
+ },
+ onEndUpdateBatch: function onEndUpdateBatch() {
+ return this.validate(arguments.callee.name, arguments);
+ },
+ onItemAdded: function onItemAdded(aItemId, aParentId, aIndex, aItemType,
+ aURI, aTitle)
+ {
+ return this.validate(arguments.callee.name, { id: aItemId,
+ index: aIndex,
+ type: aItemType,
+ url: aURI ? aURI.spec : null,
+ title: aTitle });
+ },
+ onItemRemoved: function onItemRemoved() {
+ return this.validate(arguments.callee.name, arguments);
+ },
+ onItemChanged: function onItemChanged(id, property, aIsAnno,
+ aNewValue, aLastModified, type)
+ {
+ return this.validate(arguments.callee.name,
+ { id,
+ get index() {
+ return PlacesUtils.bookmarks.getItemIndex(this.id);
+ },
+ type,
+ property,
+ get url() {
+ return type == PlacesUtils.bookmarks.TYPE_BOOKMARK ?
+ PlacesUtils.bookmarks.getBookmarkURI(this.id).spec :
+ null;
+ },
+ get title() {
+ return PlacesUtils.bookmarks.getItemTitle(this.id);
+ },
+ });
+ },
+ onItemVisited: function onItemVisited() {
+ return this.validate(arguments.callee.name, arguments);
+ },
+ onItemMoved: function onItemMoved(aItemId, aOldParentId, aOldIndex,
+ aNewParentId, aNewIndex, aItemType)
+ {
+ this.validate(arguments.callee.name, { id: aItemId,
+ index: aNewIndex,
+ type: aItemType });
+ }
+ }, false);
+}
+
+function wrapNodeByIdAndParent(aItemId, aParentId)
+{
+ let wrappedNode;
+ let root = PlacesUtils.getFolderContents(aParentId, false, false).root;
+ for (let i = 0; i < root.childCount; ++i) {
+ let node = root.getChild(i);
+ if (node.itemId == aItemId) {
+ let type;
+ if (PlacesUtils.nodeIsContainer(node)) {
+ type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
+ }
+ else if (PlacesUtils.nodeIsURI(node)) {
+ type = PlacesUtils.TYPE_X_MOZ_PLACE;
+ }
+ else if (PlacesUtils.nodeIsSeparator(node)) {
+ type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
+ }
+ else {
+ do_throw("Unknown node type");
+ }
+ wrappedNode = PlacesUtils.wrapNode(node, type);
+ }
+ }
+ root.containerOpen = false;
+ return JSON.parse(wrappedNode);
+}
+
+add_test(function test_text_paste()
+{
+ const TEST_URL = "http://places.moz.org/"
+ const TEST_TITLE = "Places bookmark"
+
+ waitForBookmarkNotification("onItemAdded", function(aData)
+ {
+ do_check_eq(aData.title, TEST_TITLE);
+ do_check_eq(aData.url, TEST_URL);
+ do_check_eq(aData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ do_check_eq(aData.index, 0);
+ run_next_test();
+ });
+
+ let txn = PlacesUIUtils.makeTransaction(
+ { title: TEST_TITLE, uri: TEST_URL },
+ PlacesUtils.TYPE_X_MOZ_URL,
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true // Unused for text.
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+});
+
+add_test(function test_container()
+{
+ const TEST_TITLE = "Places folder"
+
+ waitForBookmarkNotification("onItemChanged", function(aChangedData)
+ {
+ do_check_eq(aChangedData.title, TEST_TITLE);
+ do_check_eq(aChangedData.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ do_check_eq(aChangedData.index, 1);
+
+ waitForBookmarkNotification("onItemAdded", function(aAddedData)
+ {
+ do_check_eq(aAddedData.title, TEST_TITLE);
+ do_check_eq(aAddedData.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ do_check_eq(aAddedData.index, 2);
+ let id = aAddedData.id;
+
+ waitForBookmarkNotification("onItemMoved", function(aMovedData)
+ {
+ do_check_eq(aMovedData.id, id);
+ do_check_eq(aMovedData.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ do_check_eq(aMovedData.index, 1);
+
+ run_next_test();
+ });
+
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aAddedData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ 1, // Move to position 1.
+ false
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ });
+
+ try {
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aChangedData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ } catch (ex) {
+ do_throw(ex);
+ }
+ }, "random-anno");
+
+ let id = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_TITLE,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.annotations.setItemAnnotation(id, PlacesUIUtils.DESCRIPTION_ANNO,
+ "description", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ PlacesUtils.annotations.setItemAnnotation(id, "random-anno",
+ "random-value", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+});
+
+
+add_test(function test_separator()
+{
+ waitForBookmarkNotification("onItemChanged", function(aChangedData)
+ {
+ do_check_eq(aChangedData.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ do_check_eq(aChangedData.index, 3);
+
+ waitForBookmarkNotification("onItemAdded", function(aAddedData)
+ {
+ do_check_eq(aAddedData.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ do_check_eq(aAddedData.index, 4);
+ let id = aAddedData.id;
+
+ waitForBookmarkNotification("onItemMoved", function(aMovedData)
+ {
+ do_check_eq(aMovedData.id, id);
+ do_check_eq(aMovedData.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ do_check_eq(aMovedData.index, 1);
+
+ run_next_test();
+ });
+
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aAddedData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ 1, // Move to position 1.
+ false
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ });
+
+ try {
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aChangedData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ } catch (ex) {
+ do_throw(ex);
+ }
+ }, "random-anno");
+
+ let id = PlacesUtils.bookmarks.insertSeparator(PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.annotations.setItemAnnotation(id, "random-anno",
+ "random-value", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+});
+
+add_test(function test_bookmark()
+{
+ const TEST_URL = "http://places.moz.org/"
+ const TEST_TITLE = "Places bookmark"
+
+ waitForBookmarkNotification("onItemChanged", function(aChangedData)
+ {
+ do_check_eq(aChangedData.title, TEST_TITLE);
+ do_check_eq(aChangedData.url, TEST_URL);
+ do_check_eq(aChangedData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ do_check_eq(aChangedData.index, 5);
+
+ waitForBookmarkNotification("onItemAdded", function(aAddedData)
+ {
+ do_check_eq(aAddedData.title, TEST_TITLE);
+ do_check_eq(aAddedData.url, TEST_URL);
+ do_check_eq(aAddedData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ do_check_eq(aAddedData.index, 6);
+ let id = aAddedData.id;
+
+ waitForBookmarkNotification("onItemMoved", function(aMovedData)
+ {
+ do_check_eq(aMovedData.id, id);
+ do_check_eq(aMovedData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ do_check_eq(aMovedData.index, 1);
+
+ run_next_test();
+ });
+
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aAddedData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ 1, // Move to position 1.
+ false
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ });
+
+ try {
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aChangedData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ } catch (ex) {
+ do_throw(ex);
+ }
+ }, "random-anno");
+
+ let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ NetUtil.newURI(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TEST_TITLE);
+ PlacesUtils.annotations.setItemAnnotation(id, PlacesUIUtils.DESCRIPTION_ANNO,
+ "description", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ PlacesUtils.annotations.setItemAnnotation(id, "random-anno",
+ "random-value", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+});
+
+add_test(function test_visit()
+{
+ const TEST_URL = "http://places.moz.org/"
+ const TEST_TITLE = "Places bookmark"
+
+ waitForBookmarkNotification("onItemAdded", function(aAddedData)
+ {
+ do_check_eq(aAddedData.title, TEST_TITLE);
+ do_check_eq(aAddedData.url, TEST_URL);
+ do_check_eq(aAddedData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ do_check_eq(aAddedData.index, 7);
+
+ waitForBookmarkNotification("onItemAdded", function(aAddedData2)
+ {
+ do_check_eq(aAddedData2.title, TEST_TITLE);
+ do_check_eq(aAddedData2.url, TEST_URL);
+ do_check_eq(aAddedData2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ do_check_eq(aAddedData2.index, 8);
+ run_next_test();
+ });
+
+ try {
+ let node = wrapNodeByIdAndParent(aAddedData.id, PlacesUtils.unfiledBookmarksFolderId);
+ // Simulate a not-bookmarked node, will copy it to a new bookmark.
+ node.id = -1;
+ let txn = PlacesUIUtils.makeTransaction(
+ node,
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ } catch (ex) {
+ do_throw(ex);
+ }
+ });
+
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ NetUtil.newURI(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TEST_TITLE);
+});
+
+add_test(function check_annotations() {
+ // As last step check how many items for each annotation exist.
+
+ // Copies should retain the description annotation.
+ let descriptions =
+ PlacesUtils.annotations.getItemsWithAnnotation(PlacesUIUtils.DESCRIPTION_ANNO, {});
+ do_check_eq(descriptions.length, 4);
+
+ // Only the original bookmarks should have this annotation.
+ let others = PlacesUtils.annotations.getItemsWithAnnotation("random-anno", {});
+ do_check_eq(others.length, 3);
+ run_next_test();
+});
+
+function run_test()
+{
+ run_next_test();
+}
diff --git a/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js b/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js
new file mode 100644
index 000000000..4db21555f
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js
@@ -0,0 +1,33 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Tests that nsBrowserGlue correctly exports bookmarks.html at shutdown if
+ * browser.bookmarks.autoExportHTML is set to true.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ remove_bookmarks_html();
+
+ Services.prefs.setBoolPref("browser.bookmarks.autoExportHTML", true);
+ do_register_cleanup(() => Services.prefs.clearUserPref("browser.bookmarks.autoExportHTML"));
+
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Initialize Places through the History Service.
+ Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsINavHistoryService);
+
+ Services.obs.addObserver(function observer() {
+ Services.obs.removeObserver(observer, "profile-before-change");
+ check_bookmarks_html();
+ }, "profile-before-change", false);
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt.js b/browser/components/places/tests/unit/test_browserGlue_corrupt.js
new file mode 100644
index 000000000..5b2a09068
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_corrupt.js
@@ -0,0 +1,59 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Tests that nsBrowserGlue correctly restores bookmarks from a JSON backup if
+ * database is corrupt and one backup is available.
+ */
+
+function run_test() {
+ // Create our bookmarks.html from bookmarks.glue.html.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ remove_all_JSON_backups();
+
+ // Create our JSON backup from bookmarks.glue.json.
+ create_JSON_backup("bookmarks.glue.json");
+
+ run_next_test();
+}
+
+do_register_cleanup(function () {
+ remove_bookmarks_html();
+ remove_all_JSON_backups();
+ return PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_main() {
+ // Create a corrupt database.
+ yield createCorruptDB();
+
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Check the database was corrupt.
+ // nsBrowserGlue uses databaseStatus to manage initialization.
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+
+ // The test will continue once restore has finished and smart bookmarks
+ // have been created.
+ yield promiseTopicObserved("places-browser-init-complete");
+
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ });
+ yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+
+ // Check that JSON backup has been restored.
+ // Notice restore from JSON notification is fired before smart bookmarks creation.
+ bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_TOOLBAR
+ });
+ Assert.equal(bm.title, "examplejson");
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js
new file mode 100644
index 000000000..7cb4e5e4c
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js
@@ -0,0 +1,52 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Tests that nsBrowserGlue correctly imports from bookmarks.html if database
+ * is corrupt but a JSON backup is not available.
+ */
+
+function run_test() {
+ // Create our bookmarks.html from bookmarks.glue.html.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ // Remove JSON backup from profile.
+ remove_all_JSON_backups();
+
+ run_next_test();
+}
+
+do_register_cleanup(remove_bookmarks_html);
+
+add_task(function* () {
+ // Create a corrupt database.
+ yield createCorruptDB();
+
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Check the database was corrupt.
+ // nsBrowserGlue uses databaseStatus to manage initialization.
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+
+ // The test will continue once import has finished and smart bookmarks
+ // have been created.
+ yield promiseTopicObserved("places-browser-init-complete");
+
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ });
+ yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+
+ // Check that bookmarks html has been restored.
+ bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_TOOLBAR
+ });
+ Assert.equal(bm.title, "example");
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js
new file mode 100644
index 000000000..480420091
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Tests that nsBrowserGlue correctly restores default bookmarks if database is
+ * corrupt, nor a JSON backup nor bookmarks.html are available.
+ */
+
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
+function run_test() {
+ // Remove bookmarks.html from profile.
+ remove_bookmarks_html();
+
+ // Remove JSON backup from profile.
+ remove_all_JSON_backups();
+
+ run_next_test();
+}
+
+add_task(function* () {
+ // Create a corrupt database.
+ yield createCorruptDB();
+
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Check the database was corrupt.
+ // nsBrowserGlue uses databaseStatus to manage initialization.
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+
+ // The test will continue once import has finished and smart bookmarks
+ // have been created.
+ yield promiseTopicObserved("places-browser-init-complete");
+
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ });
+ yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+
+ // Check that default bookmarks have been restored.
+ bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_TOOLBAR
+ });
+
+ // Bug 1283076: Nightly bookmark points to Get Involved page, not Getting Started one
+ let chanTitle = AppConstants.NIGHTLY_BUILD ? "Get Involved" : "Getting Started";
+ do_check_eq(bm.title, chanTitle);
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_distribution.js b/browser/components/places/tests/unit/test_browserGlue_distribution.js
new file mode 100644
index 000000000..c3d6e1d9e
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_distribution.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that nsBrowserGlue correctly imports bookmarks from distribution.ini.
+ */
+
+const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion";
+const PREF_BMPROCESSED = "distribution.516444.bookmarksProcessed";
+const PREF_DISTRIBUTION_ID = "distribution.id";
+
+const TOPICDATA_DISTRIBUTION_CUSTOMIZATION = "force-distribution-customization";
+const TOPIC_CUSTOMIZATION_COMPLETE = "distribution-customization-complete";
+const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
+
+function run_test() {
+ // Set special pref to load distribution.ini from the profile folder.
+ Services.prefs.setBoolPref("distribution.testing.loadFromProfile", true);
+
+ // Copy distribution.ini file to the profile dir.
+ let distroDir = gProfD.clone();
+ distroDir.leafName = "distribution";
+ let iniFile = distroDir.clone();
+ iniFile.append("distribution.ini");
+ if (iniFile.exists()) {
+ iniFile.remove(false);
+ print("distribution.ini already exists, did some test forget to cleanup?");
+ }
+
+ let testDistributionFile = gTestDir.clone();
+ testDistributionFile.append("distribution.ini");
+ testDistributionFile.copyTo(distroDir, "distribution.ini");
+ Assert.ok(testDistributionFile.exists());
+
+ run_next_test();
+}
+
+do_register_cleanup(function () {
+ // Remove the distribution file, even if the test failed, otherwise all
+ // next tests will import it.
+ let iniFile = gProfD.clone();
+ iniFile.leafName = "distribution";
+ iniFile.append("distribution.ini");
+ if (iniFile.exists()) {
+ iniFile.remove(false);
+ }
+ Assert.ok(!iniFile.exists());
+});
+
+add_task(function* () {
+ // Disable Smart Bookmarks creation.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, -1);
+
+ // Initialize Places through the History Service and check that a new
+ // database has been created.
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CREATE);
+
+ // Force distribution.
+ let glue = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver)
+ glue.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_DISTRIBUTION_CUSTOMIZATION);
+
+ // Test will continue on customization complete notification.
+ yield promiseTopicObserved(TOPIC_CUSTOMIZATION_COMPLETE);
+
+ // Check the custom bookmarks exist on menu.
+ let menuItem = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: 0
+ });
+ Assert.equal(menuItem.title, "Menu Link Before");
+
+ menuItem = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: 1 + DEFAULT_BOOKMARKS_ON_MENU
+ });
+ Assert.equal(menuItem.title, "Menu Link After");
+
+ // Check no favicon or keyword exists for this bookmark
+ yield Assert.rejects(waitForResolvedPromise(() => {
+ return PlacesUtils.promiseFaviconData(menuItem.url.href);
+ }, "Favicon not found", 10), /Favicon\snot\sfound/, "Favicon not found");
+
+ let keywordItem = yield PlacesUtils.keywords.fetch({
+ url: menuItem.url.href
+ });
+ Assert.strictEqual(keywordItem, null);
+
+ // Check the custom bookmarks exist on toolbar.
+ let toolbarItem = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ });
+ Assert.equal(toolbarItem.title, "Toolbar Link Before");
+
+ // Check the custom favicon and keyword exist for this bookmark
+ let faviconItem = yield waitForResolvedPromise(() => {
+ return PlacesUtils.promiseFaviconData(toolbarItem.url.href);
+ }, "Favicon not found", 10);
+ Assert.equal(faviconItem.uri.spec, "https://example.org/favicon.png");
+ Assert.greater(faviconItem.dataLen, 0);
+ Assert.equal(faviconItem.mimeType, "image/png");
+
+ let base64Icon = "data:image/png;base64," +
+ base64EncodeString(String.fromCharCode.apply(String, faviconItem.data));
+ Assert.equal(base64Icon, SMALLPNG_DATA_URI.spec);
+
+ keywordItem = yield PlacesUtils.keywords.fetch({
+ url: toolbarItem.url.href
+ });
+ Assert.notStrictEqual(keywordItem, null);
+ Assert.equal(keywordItem.keyword, "e:t:b");
+
+ toolbarItem = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1 + DEFAULT_BOOKMARKS_ON_TOOLBAR
+ });
+ Assert.equal(toolbarItem.title, "Toolbar Link After");
+
+ // Check the bmprocessed pref has been created.
+ Assert.ok(Services.prefs.getBoolPref(PREF_BMPROCESSED));
+
+ // Check distribution prefs have been created.
+ Assert.equal(Services.prefs.getCharPref(PREF_DISTRIBUTION_ID), "516444");
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_migrate.js b/browser/components/places/tests/unit/test_browserGlue_migrate.js
new file mode 100644
index 000000000..817f10c81
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_migrate.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that nsBrowserGlue does not overwrite bookmarks imported from the
+ * migrators. They usually run before nsBrowserGlue, so if we find any
+ * bookmark on init, we should not try to import.
+ */
+
+const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion";
+
+function run_test() {
+ // Create our bookmarks.html from bookmarks.glue.html.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ // Remove current database file.
+ clearDB();
+
+ run_next_test();
+}
+
+do_register_cleanup(remove_bookmarks_html);
+
+add_task(function* test_migrate_bookmarks() {
+ // Initialize Places through the History Service and check that a new
+ // database has been created.
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CREATE);
+
+ // A migrator would run before nsBrowserGlue Places initialization, so mimic
+ // that behavior adding a bookmark and notifying the migration.
+ let bg = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver);
+ bg.observe(null, "initial-migration-will-import-default-bookmarks", null);
+
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://mozilla.org/",
+ title: "migrated"
+ });
+
+ let promise = promiseTopicObserved("places-browser-init-complete");
+ bg.observe(null, "initial-migration-did-import-default-bookmarks", null);
+ yield promise;
+
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ });
+ yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+
+ // Check the created bookmark still exists.
+ bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: SMART_BOOKMARKS_ON_MENU
+ });
+ Assert.equal(bm.title, "migrated");
+
+ // Check that we have not imported any new bookmark.
+ Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: SMART_BOOKMARKS_ON_MENU + 1
+ })));
+
+ Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_MENU
+ })));
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_prefs.js b/browser/components/places/tests/unit/test_browserGlue_prefs.js
new file mode 100644
index 000000000..9f3504636
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_prefs.js
@@ -0,0 +1,240 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that nsBrowserGlue is correctly interpreting the preferences settable
+ * by the user or by other components.
+ */
+
+const PREF_IMPORT_BOOKMARKS_HTML = "browser.places.importBookmarksHTML";
+const PREF_RESTORE_DEFAULT_BOOKMARKS = "browser.bookmarks.restore_default_bookmarks";
+const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion";
+const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML";
+
+const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
+const TOPICDATA_FORCE_PLACES_INIT = "force-places-init";
+
+var bg = Cc["@mozilla.org/browser/browserglue;1"].
+ getService(Ci.nsIObserver);
+
+function run_test() {
+ // Create our bookmarks.html from bookmarks.glue.html.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ remove_all_JSON_backups();
+
+ // Create our JSON backup from bookmarks.glue.json.
+ create_JSON_backup("bookmarks.glue.json");
+
+ run_next_test();
+}
+
+do_register_cleanup(function () {
+ remove_bookmarks_html();
+ remove_all_JSON_backups();
+
+ return PlacesUtils.bookmarks.eraseEverything();
+});
+
+function simulatePlacesInit() {
+ do_print("Simulate Places init");
+ // Force nsBrowserGlue::_initPlaces().
+ bg.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_FORCE_PLACES_INIT);
+ return promiseTopicObserved("places-browser-init-complete");
+}
+
+add_task(function* test_checkPreferences() {
+ // Initialize Places through the History Service and check that a new
+ // database has been created.
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CREATE);
+
+ // Wait for Places init notification.
+ yield promiseTopicObserved("places-browser-init-complete");
+
+ // Ensure preferences status.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML));
+
+ Assert.throws(() => Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+ Assert.throws(() => Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+});
+
+add_task(function* test_import() {
+ do_print("Import from bookmarks.html if importBookmarksHTML is true.");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ })));
+
+ // Set preferences.
+ Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+
+ yield simulatePlacesInit();
+
+ // Check bookmarks.html has been imported, and a smart bookmark has been
+ // created.
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_TOOLBAR
+ });
+ Assert.equal(bm.title, "example");
+
+ // Check preferences have been reverted.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+});
+
+add_task(function* test_import_noSmartBookmarks() {
+ do_print("import from bookmarks.html, but don't create smart bookmarks " +
+ "if they are disabled");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ })));
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, -1);
+ Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+
+ yield simulatePlacesInit();
+
+ // Check bookmarks.html has been imported, but smart bookmarks have not
+ // been created.
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ });
+ Assert.equal(bm.title, "example");
+
+ // Check preferences have been reverted.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+});
+
+add_task(function* test_import_autoExport_updatedSmartBookmarks() {
+ do_print("Import from bookmarks.html, but don't create smart bookmarks " +
+ "if autoExportHTML is true and they are at latest version");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ })));
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 999);
+ Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, true);
+ Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+
+ yield simulatePlacesInit();
+
+ // Check bookmarks.html has been imported, but smart bookmarks have not
+ // been created.
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ });
+ Assert.equal(bm.title, "example");
+
+ // Check preferences have been reverted.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+
+ Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
+});
+
+add_task(function* test_import_autoExport_oldSmartBookmarks() {
+ do_print("Import from bookmarks.html, and create smart bookmarks if " +
+ "autoExportHTML is true and they are not at latest version.");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ })));
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
+ Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, true);
+ Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+
+ yield simulatePlacesInit();
+
+ // Check bookmarks.html has been imported, but smart bookmarks have not
+ // been created.
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_TOOLBAR
+ });
+ Assert.equal(bm.title, "example");
+
+ // Check preferences have been reverted.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+
+ Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
+});
+
+add_task(function* test_restore() {
+ do_print("restore from default bookmarks.html if " +
+ "restore_default_bookmarks is true.");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ })));
+
+ // Set preferences.
+ Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true);
+
+ yield simulatePlacesInit();
+
+ // Check bookmarks.html has been restored.
+ Assert.ok(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_TOOLBAR
+ }));
+
+ // Check preferences have been reverted.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+});
+
+add_task(function* test_restore_import() {
+ do_print("setting both importBookmarksHTML and " +
+ "restore_default_bookmarks should restore defaults.");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ })));
+
+ // Set preferences.
+ Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+ Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true);
+
+ yield simulatePlacesInit();
+
+ // Check bookmarks.html has been restored.
+ Assert.ok(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_TOOLBAR
+ }));
+
+ // Check preferences have been reverted.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_restore.js b/browser/components/places/tests/unit/test_browserGlue_restore.js
new file mode 100644
index 000000000..9d7ac5ac1
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_restore.js
@@ -0,0 +1,62 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Tests that nsBrowserGlue correctly restores bookmarks from a JSON backup if
+ * database has been created and one backup is available.
+ */
+
+function run_test() {
+ // Create our bookmarks.html from bookmarks.glue.html.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ remove_all_JSON_backups();
+
+ // Create our JSON backup from bookmarks.glue.json.
+ create_JSON_backup("bookmarks.glue.json");
+
+ // Remove current database file.
+ clearDB();
+
+ run_next_test();
+}
+
+do_register_cleanup(function () {
+ remove_bookmarks_html();
+ remove_all_JSON_backups();
+ return PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_main() {
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Initialize Places through the History Service.
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+ // Check a new database has been created.
+ // nsBrowserGlue uses databaseStatus to manage initialization.
+ Assert.equal(hs.databaseStatus, hs.DATABASE_STATUS_CREATE);
+
+ // The test will continue once restore has finished and smart bookmarks
+ // have been created.
+ yield promiseTopicObserved("places-browser-init-complete");
+
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ });
+ yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+
+ // Check that JSON backup has been restored.
+ // Notice restore from JSON notification is fired before smart bookmarks creation.
+ bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_TOOLBAR
+ });
+ Assert.equal(bm.title, "examplejson");
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js b/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js
new file mode 100644
index 000000000..6ecaec4fe
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js
@@ -0,0 +1,285 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Tests that nsBrowserGlue is correctly interpreting the preferences settable
+ * by the user or by other components.
+ */
+
+const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion";
+const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML";
+const PREF_IMPORT_BOOKMARKS_HTML = "browser.places.importBookmarksHTML";
+const PREF_RESTORE_DEFAULT_BOOKMARKS = "browser.bookmarks.restore_default_bookmarks";
+
+function run_test() {
+ remove_bookmarks_html();
+ remove_all_JSON_backups();
+ run_next_test();
+}
+
+do_register_cleanup(() => PlacesUtils.bookmarks.eraseEverything());
+
+function countFolderChildren(aFolderItemId) {
+ let rootNode = PlacesUtils.getFolderContents(aFolderItemId).root;
+ let cc = rootNode.childCount;
+ // Dump contents.
+ for (let i = 0; i < cc ; i++) {
+ let node = rootNode.getChild(i);
+ let title = PlacesUtils.nodeIsSeparator(node) ? "---" : node.title;
+ print("Found child(" + i + "): " + title);
+ }
+ rootNode.containerOpen = false;
+ return cc;
+}
+
+add_task(function* setup() {
+ // Initialize browserGlue, but remove it's listener to places-init-complete.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver);
+
+ // Initialize Places.
+ PlacesUtils.history;
+
+ // Wait for Places init notification.
+ yield promiseTopicObserved("places-browser-init-complete");
+
+ // Ensure preferences status.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML));
+ Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+ Assert.throws(() => Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+});
+
+add_task(function* test_version_0() {
+ do_print("All smart bookmarks are created if smart bookmarks version is 0.");
+
+ // Sanity check: we should have default bookmark.
+ Assert.ok(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ }));
+
+ Assert.ok(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: 0
+ }));
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
+
+ yield rebuildSmartBookmarks();
+
+ // Count items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Check version has been updated.
+ Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+ SMART_BOOKMARKS_VERSION);
+});
+
+add_task(function* test_version_change() {
+ do_print("An existing smart bookmark is replaced when version changes.");
+
+ // Sanity check: we have a smart bookmark on the toolbar.
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ });
+ yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+
+ // Change its title.
+ yield PlacesUtils.bookmarks.update({guid: bm.guid, title: "new title"});
+ bm = yield PlacesUtils.bookmarks.fetch({guid: bm.guid});
+ Assert.equal(bm.title, "new title");
+
+ // Sanity check items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
+
+ yield rebuildSmartBookmarks();
+
+ // Count items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Check smart bookmark has been replaced, itemId has changed.
+ bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ });
+ yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+ Assert.notEqual(bm.title, "new title");
+
+ // Check version has been updated.
+ Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+ SMART_BOOKMARKS_VERSION);
+});
+
+add_task(function* test_version_change_pos() {
+ do_print("bookmarks position is retained when version changes.");
+
+ // Sanity check items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: 0
+ });
+ yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+ let firstItemTitle = bm.title;
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
+
+ yield rebuildSmartBookmarks();
+
+ // Count items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Check smart bookmarks are still in correct position.
+ bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: 0
+ });
+ yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+ Assert.equal(bm.title, firstItemTitle);
+
+ // Check version has been updated.
+ Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+ SMART_BOOKMARKS_VERSION);
+});
+
+add_task(function* test_version_change_pos_moved() {
+ do_print("moved bookmarks position is retained when version changes.");
+
+ // Sanity check items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ let bm1 = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: 0
+ });
+ yield checkItemHasAnnotation(bm1.guid, SMART_BOOKMARKS_ANNO);
+ let firstItemTitle = bm1.title;
+
+ // Move the first smart bookmark to the end of the menu.
+ yield PlacesUtils.bookmarks.update({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ guid: bm1.guid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX
+ });
+
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX
+ });
+ Assert.equal(bm.guid, bm1.guid);
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
+
+ yield rebuildSmartBookmarks();
+
+ // Count items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ bm1 = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX
+ });
+ yield checkItemHasAnnotation(bm1.guid, SMART_BOOKMARKS_ANNO);
+ Assert.equal(bm1.title, firstItemTitle);
+
+ // Move back the smart bookmark to the original position.
+ yield PlacesUtils.bookmarks.update({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ guid: bm1.guid,
+ index: 1
+ });
+
+ // Check version has been updated.
+ Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+ SMART_BOOKMARKS_VERSION);
+});
+
+add_task(function* test_recreation() {
+ do_print("An explicitly removed smart bookmark should not be recreated.");
+
+ // Remove toolbar's smart bookmarks
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ });
+ yield PlacesUtils.bookmarks.remove(bm.guid);
+
+ // Sanity check items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
+
+ yield rebuildSmartBookmarks();
+
+ // Count items.
+ // We should not have recreated the smart bookmark on toolbar.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Check version has been updated.
+ Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+ SMART_BOOKMARKS_VERSION);
+});
+
+add_task(function* test_recreation_version_0() {
+ do_print("Even if a smart bookmark has been removed recreate it if version is 0.");
+
+ // Sanity check items.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
+
+ yield rebuildSmartBookmarks();
+
+ // Count items.
+ // We should not have recreated the smart bookmark on toolbar.
+ Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+ SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+ Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+ SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+ // Check version has been updated.
+ Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+ SMART_BOOKMARKS_VERSION);
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_urlbar_defaultbehavior_migration.js b/browser/components/places/tests/unit/test_browserGlue_urlbar_defaultbehavior_migration.js
new file mode 100644
index 000000000..072056b3f
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_urlbar_defaultbehavior_migration.js
@@ -0,0 +1,150 @@
+/* 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 UI_VERSION = 26;
+const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
+const TOPICDATA_BROWSERGLUE_TEST = "force-ui-migration";
+const DEFAULT_BEHAVIOR_PREF = "browser.urlbar.default.behavior";
+const AUTOCOMPLETE_PREF = "browser.urlbar.autocomplete.enabled";
+
+var gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"]
+ .getService(Ci.nsIObserver);
+var gGetBoolPref = Services.prefs.getBoolPref;
+
+function run_test() {
+ run_next_test();
+}
+
+do_register_cleanup(cleanup);
+
+function cleanup() {
+ let prefix = "browser.urlbar.suggest.";
+ for (let type of ["history", "bookmark", "openpage", "history.onlyTyped"]) {
+ Services.prefs.clearUserPref(prefix + type);
+ }
+ Services.prefs.clearUserPref("browser.migration.version");
+ Services.prefs.clearUserPref(AUTOCOMPLETE_PREF);
+}
+
+function setupBehaviorAndMigrate(aDefaultBehavior, aAutocompleteEnabled = true) {
+ cleanup();
+ // Migrate browser.urlbar.default.behavior preference.
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1);
+ Services.prefs.setIntPref(DEFAULT_BEHAVIOR_PREF, aDefaultBehavior);
+ Services.prefs.setBoolPref(AUTOCOMPLETE_PREF, aAutocompleteEnabled);
+ // Simulate a migration.
+ gBrowserGlue.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_BROWSERGLUE_TEST);
+}
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 0");
+ setupBehaviorAndMigrate(0);
+
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
+ "History preference should be true.");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.bookmark"),
+ "Bookmark preference should be true.");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.openpage"),
+ "Openpage preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false.");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 1");
+ setupBehaviorAndMigrate(1);
+
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
+ "History preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.bookmark"), false,
+ "Bookmark preference should be false.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.openpage"), false,
+ "Openpage preference should be false");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 2");
+ setupBehaviorAndMigrate(2);
+
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history"), false,
+ "History preference should be false.");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.bookmark"),
+ "Bookmark preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.openpage"), false,
+ "Openpage preference should be false");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 3");
+ setupBehaviorAndMigrate(3);
+
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
+ "History preference should be true.");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.bookmark"),
+ "Bookmark preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.openpage"), false,
+ "Openpage preference should be false");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 19");
+ setupBehaviorAndMigrate(19);
+
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
+ "History preference should be true.");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.bookmark"),
+ "Bookmark preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.openpage"), false,
+ "Openpage preference should be false");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 33");
+ setupBehaviorAndMigrate(33);
+
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
+ "History preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.bookmark"), false,
+ "Bookmark preference should be false.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.openpage"), false,
+ "Openpage preference should be false");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"),
+ "Typed preference should be true");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 129");
+ setupBehaviorAndMigrate(129);
+
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
+ "History preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.bookmark"), false,
+ "Bookmark preference should be false.");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.openpage"),
+ "Openpage preference should be true");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 0, autocomplete.enabled = false");
+ setupBehaviorAndMigrate(0, false);
+
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history"), false,
+ "History preference should be false.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.bookmark"), false,
+ "Bookmark preference should be false.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.openpage"), false,
+ "Openpage preference should be false");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false");
+});
diff --git a/browser/components/places/tests/unit/test_clearHistory_shutdown.js b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
new file mode 100644
index 000000000..0c1d78801
--- /dev/null
+++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
@@ -0,0 +1,181 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Tests that requesting clear history at shutdown will really clear history.
+ */
+
+const URIS = [
+ "http://a.example1.com/"
+, "http://b.example1.com/"
+, "http://b.example2.com/"
+, "http://c.example3.com/"
+];
+
+const TOPIC_CONNECTION_CLOSED = "places-connection-closed";
+
+var EXPECTED_NOTIFICATIONS = [
+ "places-shutdown"
+, "places-will-close-connection"
+, "places-expiration-finished"
+, "places-connection-closed"
+];
+
+const UNEXPECTED_NOTIFICATIONS = [
+ "xpcom-shutdown"
+];
+
+const FTP_URL = "ftp://localhost/clearHistoryOnShutdown/";
+
+// Send the profile-after-change notification to the form history component to ensure
+// that it has been initialized.
+var formHistoryStartup = Cc["@mozilla.org/satchel/form-history-startup;1"].
+ getService(Ci.nsIObserver);
+formHistoryStartup.observe(null, "profile-after-change", null);
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+ "resource://gre/modules/FormHistory.jsm");
+
+var timeInMicroseconds = Date.now() * 1000;
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_execute() {
+ do_print("Initialize browserglue before Places");
+
+ // Avoid default bookmarks import.
+ let glue = Cc["@mozilla.org/browser/browserglue;1"].
+ getService(Ci.nsIObserver);
+ glue.observe(null, "initial-migration-will-import-default-bookmarks", null);
+ glue.observe(null, "test-initialize-sanitizer", null);
+
+
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.cache", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.offlineApps", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.history", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.downloads", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.formData", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.sessions", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.siteSettings", true);
+
+ Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true);
+
+ do_print("Add visits.");
+ for (let aUrl of URIS) {
+ yield PlacesTestUtils.addVisits({
+ uri: uri(aUrl), visitDate: timeInMicroseconds++,
+ transition: PlacesUtils.history.TRANSITION_TYPED
+ });
+ }
+ do_print("Add cache.");
+ yield storeCache(FTP_URL, "testData");
+ do_print("Add form history.");
+ yield addFormHistory();
+ Assert.equal((yield getFormHistoryCount()), 1, "Added form history");
+
+ do_print("Simulate and wait shutdown.");
+ yield shutdownPlaces();
+
+ Assert.equal((yield getFormHistoryCount()), 0, "Form history cleared");
+
+ let stmt = DBConn(true).createStatement(
+ "SELECT id FROM moz_places WHERE url = :page_url "
+ );
+
+ try {
+ URIS.forEach(function(aUrl) {
+ stmt.params.page_url = aUrl;
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ });
+ } finally {
+ stmt.finalize();
+ }
+
+ do_print("Check cache");
+ // Check cache.
+ yield checkCache(FTP_URL);
+});
+
+function addFormHistory() {
+ return new Promise(resolve => {
+ let now = Date.now() * 1000;
+ FormHistory.update({ op: "add",
+ fieldname: "testfield",
+ value: "test",
+ timesUsed: 1,
+ firstUsed: now,
+ lastUsed: now
+ },
+ { handleCompletion(reason) { resolve(); } });
+ });
+}
+
+function getFormHistoryCount() {
+ return new Promise((resolve, reject) => {
+ let count = -1;
+ FormHistory.count({ fieldname: "testfield" },
+ { handleResult(result) { count = result; },
+ handleCompletion(reason) { resolve(count); }
+ });
+ });
+}
+
+function storeCache(aURL, aContent) {
+ let cache = Services.cache2;
+ let storage = cache.diskCacheStorage(LoadContextInfo.default, false);
+
+ return new Promise(resolve => {
+ let storeCacheListener = {
+ onCacheEntryCheck: function (entry, appcache) {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+
+ onCacheEntryAvailable: function (entry, isnew, appcache, status) {
+ do_check_eq(status, Cr.NS_OK);
+
+ entry.setMetaDataElement("servertype", "0");
+ var os = entry.openOutputStream(0);
+
+ var written = os.write(aContent, aContent.length);
+ if (written != aContent.length) {
+ do_throw("os.write has not written all data!\n" +
+ " Expected: " + written + "\n" +
+ " Actual: " + aContent.length + "\n");
+ }
+ os.close();
+ entry.close();
+ resolve();
+ }
+ };
+
+ storage.asyncOpenURI(Services.io.newURI(aURL, null, null), "",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ storeCacheListener);
+ });
+}
+
+
+function checkCache(aURL) {
+ let cache = Services.cache2;
+ let storage = cache.diskCacheStorage(LoadContextInfo.default, false);
+
+ return new Promise(resolve => {
+ let checkCacheListener = {
+ onCacheEntryAvailable: function (entry, isnew, appcache, status) {
+ do_check_eq(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND);
+ resolve();
+ }
+ };
+
+ storage.asyncOpenURI(Services.io.newURI(aURL, null, null), "",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ checkCacheListener);
+ });
+}
diff --git a/browser/components/places/tests/unit/test_leftpane_corruption_handling.js b/browser/components/places/tests/unit/test_leftpane_corruption_handling.js
new file mode 100644
index 000000000..0af6f4e95
--- /dev/null
+++ b/browser/components/places/tests/unit/test_leftpane_corruption_handling.js
@@ -0,0 +1,174 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Tests that we build a working leftpane in various corruption situations.
+ */
+
+// Used to store the original leftPaneFolderId getter.
+var gLeftPaneFolderIdGetter;
+var gAllBookmarksFolderIdGetter;
+// Used to store the original left Pane status as a JSON string.
+var gReferenceHierarchy;
+var gLeftPaneFolderId;
+
+add_task(function* () {
+ // We want empty roots.
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // Sanity check.
+ Assert.ok(!!PlacesUIUtils);
+
+ // Check getters.
+ gLeftPaneFolderIdGetter = Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId");
+ Assert.equal(typeof(gLeftPaneFolderIdGetter.get), "function");
+ gAllBookmarksFolderIdGetter = Object.getOwnPropertyDescriptor(PlacesUIUtils, "allBookmarksFolderId");
+ Assert.equal(typeof(gAllBookmarksFolderIdGetter.get), "function");
+
+ do_register_cleanup(() => PlacesUtils.bookmarks.eraseEverything());
+});
+
+add_task(function* () {
+ // Add a third party bogus annotated item. Should not be removed.
+ let folder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test",
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER
+ });
+
+ let folderId = yield PlacesUtils.promiseItemId(folder.guid);
+ PlacesUtils.annotations.setItemAnnotation(folderId, ORGANIZER_QUERY_ANNO,
+ "test", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ // Create the left pane, and store its current status, it will be used
+ // as reference value.
+ gLeftPaneFolderId = PlacesUIUtils.leftPaneFolderId;
+ gReferenceHierarchy = folderIdToHierarchy(gLeftPaneFolderId);
+
+ while (gTests.length) {
+ // Run current test.
+ yield Task.spawn(gTests.shift());
+
+ // Regenerate getters.
+ Object.defineProperty(PlacesUIUtils, "leftPaneFolderId", gLeftPaneFolderIdGetter);
+ gLeftPaneFolderId = PlacesUIUtils.leftPaneFolderId;
+ Object.defineProperty(PlacesUIUtils, "allBookmarksFolderId", gAllBookmarksFolderIdGetter);
+
+ // Check the new left pane folder.
+ let leftPaneHierarchy = folderIdToHierarchy(gLeftPaneFolderId)
+ Assert.equal(gReferenceHierarchy, leftPaneHierarchy);
+
+ folder = yield PlacesUtils.bookmarks.fetch({guid: folder.guid});
+ Assert.equal(folder.title, "test");
+ }
+});
+
+// Corruption cases.
+var gTests = [
+
+ function* test1() {
+ print("1. Do nothing, checks test calibration.");
+ },
+
+ function* test2() {
+ print("2. Delete the left pane folder.");
+ let guid = yield PlacesUtils.promiseItemGuid(gLeftPaneFolderId);
+ yield PlacesUtils.bookmarks.remove(guid);
+ },
+
+ function* test3() {
+ print("3. Delete a child of the left pane folder.");
+ let guid = yield PlacesUtils.promiseItemGuid(gLeftPaneFolderId);
+ let bm = yield PlacesUtils.bookmarks.fetch({parentGuid: guid, index: 0});
+ yield PlacesUtils.bookmarks.remove(bm.guid);
+ },
+
+ function* test4() {
+ print("4. Delete AllBookmarks.");
+ let guid = yield PlacesUtils.promiseItemGuid(PlacesUIUtils.allBookmarksFolderId);
+ yield PlacesUtils.bookmarks.remove(guid);
+ },
+
+ function* test5() {
+ print("5. Create a duplicated left pane folder.");
+ let folder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "PlacesRoot",
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER
+ });
+
+ let folderId = yield PlacesUtils.promiseItemId(folder.guid);
+ PlacesUtils.annotations.setItemAnnotation(folderId, ORGANIZER_FOLDER_ANNO,
+ "PlacesRoot", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ },
+
+ function* test6() {
+ print("6. Create a duplicated left pane query.");
+ let folder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "AllBookmarks",
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER
+ });
+
+ let folderId = yield PlacesUtils.promiseItemId(folder.guid);
+ PlacesUtils.annotations.setItemAnnotation(folderId, ORGANIZER_QUERY_ANNO,
+ "AllBookmarks", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ },
+
+ function* test7() {
+ print("7. Remove the left pane folder annotation.");
+ PlacesUtils.annotations.removeItemAnnotation(gLeftPaneFolderId,
+ ORGANIZER_FOLDER_ANNO);
+ },
+
+ function* test8() {
+ print("8. Remove a left pane query annotation.");
+ PlacesUtils.annotations.removeItemAnnotation(PlacesUIUtils.allBookmarksFolderId,
+ ORGANIZER_QUERY_ANNO);
+ },
+
+ function* test9() {
+ print("9. Remove a child of AllBookmarks.");
+ let guid = yield PlacesUtils.promiseItemGuid(PlacesUIUtils.allBookmarksFolderId);
+ let bm = yield PlacesUtils.bookmarks.fetch({parentGuid: guid, index: 0});
+ yield PlacesUtils.bookmarks.remove(bm.guid);
+ }
+
+];
+
+/**
+ * Convert a folder item id to a JSON representation of it and its contents.
+ */
+function folderIdToHierarchy(aFolderId) {
+ let root = PlacesUtils.getFolderContents(aFolderId).root;
+ let hier = JSON.stringify(hierarchyToObj(root));
+ root.containerOpen = false;
+ return hier;
+}
+
+function hierarchyToObj(aNode) {
+ let o = {}
+ o.title = aNode.title;
+ o.annos = PlacesUtils.getAnnotationsForItem(aNode.itemId)
+ if (PlacesUtils.nodeIsURI(aNode)) {
+ o.uri = aNode.uri;
+ }
+ else if (PlacesUtils.nodeIsFolder(aNode)) {
+ o.children = [];
+ PlacesUtils.asContainer(aNode).containerOpen = true;
+ for (let i = 0; i < aNode.childCount; ++i) {
+ o.children.push(hierarchyToObj(aNode.getChild(i)));
+ }
+ aNode.containerOpen = false;
+ }
+ return o;
+}
diff --git a/browser/components/places/tests/unit/xpcshell.ini b/browser/components/places/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..1c40e1c53
--- /dev/null
+++ b/browser/components/places/tests/unit/xpcshell.ini
@@ -0,0 +1,25 @@
+[DEFAULT]
+head = head_bookmarks.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+ bookmarks.glue.html
+ bookmarks.glue.json
+ corruptDB.sqlite
+ distribution.ini
+
+[test_421483.js]
+[test_browserGlue_bookmarkshtml.js]
+[test_browserGlue_corrupt.js]
+[test_browserGlue_corrupt_nobackup.js]
+[test_browserGlue_corrupt_nobackup_default.js]
+[test_browserGlue_distribution.js]
+[test_browserGlue_migrate.js]
+[test_browserGlue_prefs.js]
+[test_browserGlue_restore.js]
+[test_browserGlue_smartBookmarks.js]
+[test_browserGlue_urlbar_defaultbehavior_migration.js]
+[test_clearHistory_shutdown.js]
+[test_leftpane_corruption_handling.js]
+[test_PUIU_makeTransaction.js]