From 1bada3664ba9440481ce538a44e306599db833ba Mon Sep 17 00:00:00 2001 From: Roy Tam Date: Fri, 10 Jul 2020 23:23:35 +0800 Subject: [PATCH] import changes from `dev' branch of rmottola/Arctic-Fox: - Bug 951651 - Make bookmarkProperties, Star UI and Library info pane work with PlacesTransactions. r=mak (6d0127aa5) - Bug 547623 - Add a button to about:support to enter safe mode. r=adw (1a0481412) - Bug 728813 - Switch Help menu item to allow leaving Safe Mode when it's enabled. r=MattN (cfeae5bf8) - Bug 1125115 - Write a new keywords pseudo-API in PlacesUtils. r=ttaubert (afc6f00c9) --- browser/base/content/baseMenuOverlay.xul | 4 +- browser/base/content/browser-places.js | 236 +++- browser/base/content/browser.js | 63 +- .../content/test/general/browser_syncui.js | 110 ++ browser/base/jar.mn | 2 +- browser/components/nsBrowserGlue.js | 32 + browser/components/places/PlacesUIUtils.jsm | 151 ++ .../places/content/bookmarkProperties.js | 255 ++-- .../components/places/content/controller.js | 20 +- .../places/content/editBookmarkOverlay.js | 1229 +++++++++-------- .../places/content/editBookmarkOverlay.xul | 43 +- browser/components/places/content/places.js | 93 +- browser/components/places/content/treeView.js | 9 +- .../en-US/chrome/browser/baseMenuOverlay.dtd | 2 + toolkit/components/places/Database.cpp | 82 +- toolkit/components/places/Database.h | 3 +- toolkit/components/places/PlacesDBUtils.jsm | 19 +- toolkit/components/places/PlacesUtils.jsm | 99 +- toolkit/components/places/UnifiedComplete.js | 27 +- .../places/nsINavBookmarksService.idl | 8 +- toolkit/components/places/nsNavBookmarks.cpp | 452 +++--- toolkit/components/places/nsNavBookmarks.h | 10 +- .../components/places/nsPlacesAutoComplete.js | 14 +- toolkit/components/places/nsPlacesIndexes.h | 9 + toolkit/components/places/nsPlacesTables.h | 2 + toolkit/components/places/nsPlacesTriggers.h | 40 +- .../tests/autocomplete/test_keyword_search.js | 20 +- .../places/tests/bookmarks/test_keywords.js | 372 +++-- .../components/places/tests/head_common.js | 16 +- .../places/tests/migration/places_v26.sqlite | Bin 1179648 -> 1179648 bytes .../places/tests/migration/places_v27.sqlite | Bin 0 -> 1212416 bytes .../tests/migration/test_current_from_v26.js | 75 + .../places/tests/migration/xpcshell.ini | 2 + .../places/tests/queries/test_sorting.js | 2 +- .../unifiedcomplete/test_keyword_search.js | 112 +- .../test_keyword_search_actions.js | 32 +- .../places/tests/unit/test_398914.js | 132 +- .../places/tests/unit/test_421180.js | 94 -- .../tests/unit/test_async_transactions.js | 6 +- .../places/tests/unit/test_keywords.js | 488 +++++++ .../places/tests/unit/test_placesTxn.js | 51 +- .../tests/unit/test_preventive_maintenance.js | 61 +- .../places/tests/unit/test_telemetry.js | 4 +- toolkit/content/aboutSupport.js | 23 +- toolkit/content/aboutSupport.xhtml | 6 - .../en-US/chrome/global/aboutSupport.dtd | 3 - 46 files changed, 2737 insertions(+), 1776 deletions(-) create mode 100644 browser/base/content/test/general/browser_syncui.js create mode 100644 toolkit/components/places/tests/migration/places_v27.sqlite create mode 100644 toolkit/components/places/tests/migration/test_current_from_v26.js delete mode 100644 toolkit/components/places/tests/unit/test_421180.js create mode 100644 toolkit/components/places/tests/unit/test_keywords.js diff --git a/browser/base/content/baseMenuOverlay.xul b/browser/base/content/baseMenuOverlay.xul index c6c1b16dc7..e34b580f12 100644 --- a/browser/base/content/baseMenuOverlay.xul +++ b/browser/base/content/baseMenuOverlay.xul @@ -65,7 +65,9 @@ + stopaccesskey="&helpSafeMode.stop.accesskey;" + stoplabel="&helpSafeMode.stop.label;" + oncommand="safeModeRestart();"/> { + let obs = (subject, topic, data) => { + Services.obs.removeObserver(obs, topic); + resolve(subject); + } + Services.obs.addObserver(obs, topic, false); + }); +} + +add_task(function* prepare() { + let xps = Components.classes["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + yield xps.whenLoaded(); + // mock out the "_needsSetup()" function so we don't short-circuit. + let oldNeedsSetup = window.gSyncUI._needsSetup; + window.gSyncUI._needsSetup = () => false; + registerCleanupFunction(() => { + window.gSyncUI._needsSetup = oldNeedsSetup; + // this test leaves the tab focused which can cause browser_tabopen_reflows + // to fail, so re-select the URLBar. + gURLBar.select(); + }); +}); + +add_task(function* testProlongedError() { + let promiseNotificationAdded = promiseObserver("weave:notification:added"); + Assert.equal(Notifications.notifications.length, 0, "start with no notifications"); + + // Pretend we are in the "prolonged error" state. + Weave.Status.sync = Weave.PROLONGED_SYNC_FAILURE; + Weave.Status.login = Weave.LOGIN_SUCCEEDED; + Services.obs.notifyObservers(null, "weave:ui:sync:error", null); + + let subject = yield promiseNotificationAdded; + let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract! + Assert.equal(notification.title, stringBundle.GetStringFromName("error.sync.title")); + Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification"); + + // Now pretend we just had a successful sync - the error notification should go away. + let promiseNotificationRemoved = promiseObserver("weave:notification:removed"); + Weave.Status.sync = Weave.STATUS_OK; + Services.obs.notifyObservers(null, "weave:ui:sync:finish", null); + yield promiseNotificationRemoved; + Assert.equal(Notifications.notifications.length, 0, "no notifications left"); +}); + +add_task(function* testLoginError() { + let promiseNotificationAdded = promiseObserver("weave:notification:added"); + Assert.equal(Notifications.notifications.length, 0, "start with no notifications"); + + // Pretend we are in the "prolonged error" state. + Weave.Status.sync = Weave.LOGIN_FAILED; + Weave.Status.login = Weave.LOGIN_FAILED_LOGIN_REJECTED; + Services.obs.notifyObservers(null, "weave:ui:sync:error", null); + + let subject = yield promiseNotificationAdded; + let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract! + Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title")); + Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification"); + // Now pretend we just had a successful login - the error notification should go away. + Weave.Status.sync = Weave.STATUS_OK; + Weave.Status.login = Weave.LOGIN_SUCCEEDED; + let promiseNotificationRemoved = promiseObserver("weave:notification:removed"); + Services.obs.notifyObservers(null, "weave:service:login:start", null); + Services.obs.notifyObservers(null, "weave:service:login:finish", null); + yield promiseNotificationRemoved; + Assert.equal(Notifications.notifications.length, 0, "no notifications left"); +}); + +function testButtonActions(startNotification, endNotification) { + let button = document.getElementById("sync-button"); + Assert.ok(button, "button exists"); + let panelbutton = document.getElementById("PanelUI-fxa-status"); + Assert.ok(panelbutton, "panel button exists"); + + // pretend a sync is starting. + Services.obs.notifyObservers(null, startNotification, null); + Assert.equal(button.getAttribute("status"), "active"); + Assert.equal(panelbutton.getAttribute("syncstatus"), "active"); + + Services.obs.notifyObservers(null, endNotification, null); + Assert.ok(!button.hasAttribute("status")); + Assert.ok(!panelbutton.hasAttribute("syncstatus")); +} + +add_task(function* testButtonActivities() { + // add the Sync button to the panel so we can get it! + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + // check the button's functionality + yield PanelUI.show(); + try { + testButtonActions("weave:service:login:start", "weave:service:login:finish"); + testButtonActions("weave:service:sync:start", "weave:ui:sync:finish"); + } finally { + PanelUI.hide(); + CustomizableUI.removeWidgetFromArea("sync-button"); + } +}); diff --git a/browser/base/jar.mn b/browser/base/jar.mn index 5b16b383a7..247d92feaa 100644 --- a/browser/base/jar.mn +++ b/browser/base/jar.mn @@ -102,7 +102,7 @@ browser.jar: content/browser/openLocation.xul (content/openLocation.xul) content/browser/safeMode.css (content/safeMode.css) content/browser/safeMode.js (content/safeMode.js) - content/browser/safeMode.xul (content/safeMode.xul) +* content/browser/safeMode.xul (content/safeMode.xul) * content/browser/sanitize.js (content/sanitize.js) * content/browser/sanitize.xul (content/sanitize.xul) * content/browser/sanitizeDialog.js (content/sanitizeDialog.js) diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index e0b4ebf04c..baf179b443 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -192,6 +192,9 @@ BrowserGlue.prototype = { Services.console.logStringMessage(null); // clear the console (in case it's open) Services.console.reset(); break; + case "restart-in-safe-mode": + this._onSafeModeRestart(); + break; case "quit-application-requested": this._onQuitRequest(subject, data); break; @@ -368,6 +371,7 @@ BrowserGlue.prototype = { os.addObserver(this, "profile-before-change", false); os.addObserver(this, "browser-search-engine-modified", false); os.addObserver(this, "browser-search-service", false); + os.addObserver(this, "restart-in-safe-mode", false); }, // cleanup (called on application shutdown) @@ -379,6 +383,7 @@ BrowserGlue.prototype = { os.removeObserver(this, "browser:purge-session-history"); os.removeObserver(this, "quit-application-requested"); os.removeObserver(this, "quit-application-granted"); + os.removeObserver(this, "restart-in-safe-mode"); #ifdef OBSERVE_LASTWINDOW_CLOSE_TOPICS os.removeObserver(this, "browser-lastwindow-close-requested"); os.removeObserver(this, "browser-lastwindow-close-granted"); @@ -542,6 +547,33 @@ BrowserGlue.prototype = { } }, + _onSafeModeRestart: function BG_onSafeModeRestart() { + // prompt the user to confirm + let strings = Services.strings.createBundle("chrome://browser/locale/browser.properties"); + let promptTitle = strings.GetStringFromName("safeModeRestartPromptTitle"); + let promptMessage = strings.GetStringFromName("safeModeRestartPromptMessage"); + let restartText = strings.GetStringFromName("safeModeRestartButton"); + let buttonFlags = (Services.prompt.BUTTON_POS_0 * + Services.prompt.BUTTON_TITLE_IS_STRING) + + (Services.prompt.BUTTON_POS_1 * + Services.prompt.BUTTON_TITLE_CANCEL) + + Services.prompt.BUTTON_POS_0_DEFAULT; + + let rv = Services.prompt.confirmEx(null, promptTitle, promptMessage, + buttonFlags, restartText, null, null, + null, {}); + if (rv != 0) + return; + + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); + + if (!cancelQuit.data) { + Services.startup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit); + } + }, + _trackSlowStartup: function () { if (Services.startup.interrupted || Services.prefs.getBoolPref("browser.slowStartup.notificationDisabled")) diff --git a/browser/components/places/PlacesUIUtils.jsm b/browser/components/places/PlacesUIUtils.jsm index 5a06e4d892..3eb5bcee13 100644 --- a/browser/components/places/PlacesUIUtils.jsm +++ b/browser/components/places/PlacesUIUtils.jsm @@ -18,6 +18,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", "resource:///modules/RecentWindow.jsm"); @@ -1153,6 +1155,155 @@ this.PlacesUIUtils = { Weave.Service.engineManager.get("tabs") && Weave.Service.engineManager.get("tabs").enabled; }, + + /** + * 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)); + }) }; XPCOMUtils.defineLazyServiceGetter(PlacesUIUtils, "RDF", diff --git a/browser/components/places/content/bookmarkProperties.js b/browser/components/places/content/bookmarkProperties.js index 22bd517735..bd22f57c5f 100644 --- a/browser/components/places/content/bookmarkProperties.js +++ b/browser/components/places/content/bookmarkProperties.js @@ -37,9 +37,11 @@ * - "edit" - for editing a bookmark item or a folder. * @ type (String). Possible values: * - "bookmark" - * @ itemId (Integer) - the id of the bookmark item. + * @ node (an nsINavHistoryResultNode object) - a node representing + * the bookmark. * - "folder" (also applies to livemarks) - * @ itemId (Integer) - the id of the folder. + * @ 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: @@ -49,10 +51,7 @@ * - "keyword" * - "tags" * - "loadInSidebar" - * - "feedLocation" - * - "siteLocation" * - "folderPicker" - hides both the tree and the menu. - * @ readOnly (Boolean) - optional, states if the panel should be read-only * * window.arguments[0].performed is set to true if any transaction has * been performed by the dialog. @@ -61,6 +60,10 @@ 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; @@ -99,7 +102,6 @@ var BookmarkPropertiesPanel = { _defaultInsertionPoint: null, _hiddenRows: [], _batching: false, - _readOnly: false, /** * This method returns the correct label for the dialog's "accept" @@ -148,8 +150,8 @@ var BookmarkPropertiesPanel = { /** * Determines the initial data for the item edited or added by this dialog */ - _determineItemInfo: function BPP__determineItemInfo() { - var dialogInfo = window.arguments[0]; + _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) { @@ -161,11 +163,12 @@ var BookmarkPropertiesPanel = { if ("defaultInsertionPoint" in dialogInfo) { this._defaultInsertionPoint = dialogInfo.defaultInsertionPoint; } - else + else { this._defaultInsertionPoint = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, PlacesUtils.bookmarks.DEFAULT_INDEX, Ci.nsITreeView.DROP_ON); + } switch (dialogInfo.type) { case "bookmark": @@ -232,52 +235,15 @@ var BookmarkPropertiesPanel = { this._description = dialogInfo.description; } else { // edit - NS_ASSERT("itemId" in dialogInfo); - this._itemId = dialogInfo.itemId; - this._title = PlacesUtils.bookmarks.getItemTitle(this._itemId); - this._readOnly = !!dialogInfo.readOnly; - + this._node = dialogInfo.node; switch (dialogInfo.type) { case "bookmark": this._itemType = BOOKMARK_ITEM; - - this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId); - // keyword - this._keyword = PlacesUtils.bookmarks - .getKeywordForBookmark(this._itemId); - // Load In Sidebar - this._loadInSidebar = PlacesUtils.annotations - .itemHasAnnotation(this._itemId, - PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); break; - case "folder": this._itemType = BOOKMARK_FOLDER; - PlacesUtils.livemarks.getLivemark({ id: this._itemId }) - .then(aLivemark => { - this._itemType = LIVEMARK_CONTAINER; - this._feedURI = aLivemark.feedURI; - this._siteURI = aLivemark.siteURI; - this._fillEditProperties(); - - let acceptButton = document.documentElement.getButton("accept"); - acceptButton.disabled = !this._inputIsValid(); - - let newHeight = window.outerHeight + - this._element("descriptionField").boxObject.height; - window.resizeTo(window.outerWidth, newHeight); - }, () => undefined); - break; } - - // Description - if (PlacesUtils.annotations - .itemHasAnnotation(this._itemId, PlacesUIUtils.DESCRIPTION_ANNO)) { - this._description = PlacesUtils.annotations - .getItemAnnotation(this._itemId, - PlacesUIUtils.DESCRIPTION_ANNO); - } } }, @@ -303,7 +269,7 @@ var BookmarkPropertiesPanel = { * This method should be called by the onload of the Bookmark Properties * dialog to initialize the state of the panel. */ - onDialogLoad: function BPP_onDialogLoad() { + onDialogLoad: Task.async(function* () { this._determineItemInfo(); document.title = this._getDialogTitle(); @@ -351,11 +317,23 @@ var BookmarkPropertiesPanel = { switch (this._action) { case ACTION_EDIT: - this._fillEditProperties(); - acceptButton.disabled = this._readOnly; + gEditItemOverlay.initPanel({ node: this._node + , hiddenRows: this._hiddenRows }); + acceptButton.disabled = gEditItemOverlay.readOnly; break; case ACTION_ADD: - this._fillAddProperties(); + this._node = yield this._promiseNewItem(); + // Edit the new item + gEditItemOverlay.initPanel({ node: this._node + , hiddenRows: this._hiddenRows }); + + // Empty location field if the uri is about:blank, this way inserting a new + // url will be easier for the user, Accept button will be automatically + // disabled by the input listener until the user fills the field. + 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) @@ -363,7 +341,12 @@ var BookmarkPropertiesPanel = { break; } - if (!this._readOnly) { + // Adjust the dialog size to the changes done by initPanel. This is necessary because + // initPanel, which shows and hides elements, may run after some async work was done + // here - i.e. after the DOM load event was processed. + window.sizeToContent(); + + if (!gEditItemOverlay.readOnly) { // Listen on uri fields to enable accept button if input is valid if (this._itemType == BOOKMARK_ITEM) { this._element("locationField") @@ -373,14 +356,8 @@ var BookmarkPropertiesPanel = { .addEventListener("input", this, false); } } - else if (this._itemType == LIVEMARK_CONTAINER) { - this._element("feedLocationField") - .addEventListener("input", this, false); - this._element("siteLocationField") - .addEventListener("input", this, false); - } } - }, + }), // nsIDOMEventListener handleEvent: function BPP_handleEvent(aEvent) { @@ -388,8 +365,6 @@ var BookmarkPropertiesPanel = { switch (aEvent.type) { case "input": if (target.id == "editBMPanel_locationField" || - target.id == "editBMPanel_feedLocationField" || - target.id == "editBMPanel_siteLocationField" || target.id == "editBMPanel_keywordField") { // Check uri fields to enable accept button if input is valid document.documentElement @@ -406,41 +381,39 @@ var BookmarkPropertiesPanel = { } }, - _beginBatch: function BPP__beginBatch() { + // 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; - - PlacesUtils.transactionManager.beginBatch(null); + 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: function BPP__endBatch() { + _endBatch() { if (!this._batching) return; - PlacesUtils.transactionManager.endBatch(false); + if (PlacesUIUtils.useAsyncTransactions) { + this._batchBlockingDeferred.resolve(); + this._batchBlockingDeferred = null; + } + else { + PlacesUtils.transactionManager.endBatch(false); + } this._batching = false; }, - _fillEditProperties: function BPP__fillEditProperties() { - gEditItemOverlay.initPanel(this._itemId, - { hiddenRows: this._hiddenRows, - forceReadOnly: this._readOnly }); - }, - - _fillAddProperties: function BPP__fillAddProperties() { - this._createNewItem(); - // Edit the new item - gEditItemOverlay.initPanel(this._itemId, - { hiddenRows: this._hiddenRows }); - // Empty location field if the uri is about:blank, this way inserting a new - // url will be easier for the user, Accept button will be automatically - // disabled by the input listener until the user fills the field. - var locationField = this._element("locationField"); - if (locationField.value == "about:blank") - locationField.value = ""; - }, - // nsISupports QueryInterface: function BPP_QueryInterface(aIID) { if (aIID.equals(Ci.nsIDOMEventListener) || @@ -454,7 +427,7 @@ var BookmarkPropertiesPanel = { return document.getElementById("editBMPanel_" + aID); }, - onDialogUnload: function BPP_onDialogUnload() { + onDialogUnload() { // gEditItemOverlay does not exist anymore here, so don't rely on it. this._mutationObserver.disconnect(); delete this._mutationObserver; @@ -465,13 +438,9 @@ var BookmarkPropertiesPanel = { // currently registered EventListener on the EventTarget has no effect. this._element("locationField") .removeEventListener("input", this, false); - this._element("feedLocationField") - .removeEventListener("input", this, false); - this._element("siteLocationField") - .removeEventListener("input", this, false); }, - onDialogAccept: function BPP_onDialogAccept() { + 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 @@ -481,13 +450,16 @@ var BookmarkPropertiesPanel = { window.arguments[0].performed = true; }, - onDialogCancel: function BPP_onDialogCancel() { + onDialogCancel() { // The order here is important! We have to uninit the panel first, otherwise // changes done as part of Undo may change the panel contents and by // that force it to commit more transactions. gEditItemOverlay.uninitPanel(true); this._endBatch(); - PlacesUtils.transactionManager.undoTransaction(); + if (PlacesUIUtils.useAsyncTransactions) + PlacesTransactions.undo().catch(Components.utils.reportError); + else + PlacesUtils.transactionManager.undoTransaction(); window.arguments[0].performed = false; }, @@ -593,15 +565,14 @@ var BookmarkPropertiesPanel = { */ _getTransactionsForURIList: function BPP__getTransactionsForURIList() { var transactions = []; - for (var i = 0; i < this._URIs.length; ++i) { - var uri = this._URIs[i]; - var title = this._getURITitleFromHistory(uri); - var createTxn = new PlacesCreateBookmarkTransaction(uri, -1, + for (let uri of this._URIs) { + let title = this._getURITitleFromHistory(uri); + let createTxn = new PlacesCreateBookmarkTransaction(uri, -1, PlacesUtils.bookmarks.DEFAULT_INDEX, title); transactions.push(createTxn); } - return transactions; + return transactions; }, /** @@ -634,9 +605,6 @@ var BookmarkPropertiesPanel = { aContainer, aIndex); }, - /** - * Dialog-accept code-path for creating a new item (any type) - */ _createNewItem: function BPP__getCreateItemTransaction() { var [container, index] = this._getInsertionPointDetails(); var txn; @@ -647,12 +615,93 @@ var BookmarkPropertiesPanel = { break; case LIVEMARK_CONTAINER: txn = this._getCreateNewLivemarkTransaction(container, index); - break; + break; default: // BOOKMARK_ITEM txn = this._getCreateNewBookmarkTransaction(container, index); } PlacesUtils.transactionManager.doTransaction(txn); this._itemId = PlacesUtils.bookmarks.getIdForItemAt(container, index); - } + + return Object.freeze({ + itemId: this._itemId, + get bookmarkGuid() { + throw new Error("Node-like bookmarkGuid getter called even though " + + "async transactions are disabled"); + }, + 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 txnFunc = + { [BOOKMARK_FOLDER]: PlacesTransactions.NewFolder, + [LIVEMARK_CONTAINER]: PlacesTransactions.NewLivemark, + [BOOKMARK_ITEM]: PlacesTransactions.NewBookmark + }[this._itemType]; + + 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/controller.js b/browser/components/places/content/controller.js index 4d3773905f..a3c196208d 100644 --- a/browser/components/places/content/controller.js +++ b/browser/components/places/content/controller.js @@ -659,27 +659,13 @@ PlacesController.prototype = { /** * Opens the bookmark properties for the selected URI Node. */ - showBookmarkPropertiesForSelection: - function PC_showBookmarkPropertiesForSelection() { - var node = this._view.selectedNode; + showBookmarkPropertiesForSelection() { + let node = this._view.selectedNode; if (!node) return; - var itemType = PlacesUtils.nodeIsFolder(node) || - PlacesUtils.nodeIsTagQuery(node) ? "folder" : "bookmark"; - var concreteId = PlacesUtils.getConcreteItemId(node); - var isRootItem = PlacesUtils.isRootItem(concreteId); - var itemId = node.itemId; - if (isRootItem || PlacesUtils.nodeIsTagQuery(node)) { - // If this is a root or the Tags query we use the concrete itemId to catch - // the correct title for the node. - itemId = concreteId; - } - PlacesUIUtils.showBookmarkDialog({ action: "edit" - , type: itemType - , itemId: itemId - , readOnly: isRootItem + , node , hiddenRows: [ "folderPicker" ] }, window.top); }, diff --git a/browser/components/places/content/editBookmarkOverlay.js b/browser/components/places/content/editBookmarkOverlay.js index 98bfcccd77..a88c2ef9dd 100644 --- a/browser/components/places/content/editBookmarkOverlay.js +++ b/browser/components/places/content/editBookmarkOverlay.js @@ -2,250 +2,270 @@ * 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 = { - _uri: null, - _itemId: -1, - _itemIds: [], - _uris: [], - _tags: [], - _allTags: [], - _multiEdit: false, - _itemType: -1, - _readOnly: false, - _hiddenRows: [], +let gEditItemOverlay = { _observersAdded: false, _staticFoldersListBuilt: false, - _initialized: false, - _titleOverride: "", + + _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 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; + if (node && "parent" in node) { + let parent = node.parent; + if (parent) { + isParentReadOnly = !PlacesUtils.nodeIsFolder(parent) || + PlacesUIUtils.isContentsReadOnly(parent); + } + } + + return this._paneInfo = { itemId, itemGuid, isItem, + isURI, uri, title, + isBookmark, isFolderShortcut, isParentReadOnly, + bulkTagging, uris, + visibleRows }; + }, + + 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() { + // Bug 1120314 - Folder shortcuts are read-only due to some quirky implementation + // details (the most important being the "smart" semantics of node.title). + return (!this.initialized || + (!this._paneInfo.visibleRows.has("tagsRow") && + (this._paneInfo.isFolderShortcut || + this._paneInfo.isParentReadOnly))); + }, // the first field which was edited after this panel was initialized for // a certain item _firstEditedField: "", - get itemId() { - return this._itemId; + _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 || ""); }, - get uri() { - return this._uri; + _initLocationField() { + if (!this._paneInfo.isURI) + throw new Error("_initLocationField called unexpectedly"); + this._initTextField(this._locationField, this._paneInfo.uri.spec); }, - get multiEdit() { - return this._multiEdit; + _initDescriptionField() { + if (!this._paneInfo.isItem) + throw new Error("_initDescriptionField called unexpectedly"); + + this._initTextField(this._descriptionField, + PlacesUIUtils.getItemDescription(this._paneInfo.itemId)); }, + _initKeywordField: Task.async(function* (aNewKeyword) { + if (!this._paneInfo.isBookmark) + throw new Error("_initKeywordField called unexpectedly"); + + let newKeyword = aNewKeyword; + if (newKeyword === undefined) { + let itemId = this._paneInfo.itemId; + newKeyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId); + } + this._initTextField(this._keywordField, newKeyword); + }), + + _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); + }), + /** - * Determines the initial data for the item edited or added by this dialog + * 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" */ - _determineInfo: function EIO__determineInfo(aInfo) { - // hidden rows - if (aInfo && aInfo.hiddenRows) - this._hiddenRows = aInfo.hiddenRows; - else - this._hiddenRows.splice(0, this._hiddenRows.length); - // force-read-only - this._readOnly = aInfo && aInfo.forceReadOnly; - this._titleOverride = aInfo && aInfo.titleOverride ? aInfo.titleOverride - : ""; - }, + initPanel(aInfo) { + if (typeof(aInfo) != "object" || aInfo === null) + throw new Error("aInfo must be an object."); - _showHideRows: function EIO__showHideRows() { - var isBookmark = this._itemId != -1 && - this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK; - var isQuery = false; - if (this._uri) - isQuery = this._uri.schemeIs("place"); - - this._element("nameRow").collapsed = this._hiddenRows.indexOf("name") != -1; - this._element("folderRow").collapsed = - this._hiddenRows.indexOf("folderPicker") != -1 || this._readOnly; - this._element("tagsRow").collapsed = !this._uri || - this._hiddenRows.indexOf("tags") != -1 || isQuery; - // Collapse the tag selector if the item does not accept tags. - if (!this._element("tagsSelectorRow").collapsed && - this._element("tagsRow").collapsed) - this.toggleTagsSelector(); - this._element("descriptionRow").collapsed = - this._hiddenRows.indexOf("description") != -1 || this._readOnly; - this._element("keywordRow").collapsed = !isBookmark || this._readOnly || - this._hiddenRows.indexOf("keyword") != -1 || isQuery; - this._element("locationRow").collapsed = !(this._uri && !isQuery) || - this._hiddenRows.indexOf("location") != -1; - this._element("loadInSidebarCheckbox").collapsed = !isBookmark || isQuery || - this._readOnly || this._hiddenRows.indexOf("loadInSidebar") != -1; - this._element("feedLocationRow").collapsed = !this._isLivemark || - this._hiddenRows.indexOf("feedLocation") != -1; - this._element("siteLocationRow").collapsed = !this._isLivemark || - this._hiddenRows.indexOf("siteLocation") != -1; - this._element("selectionCount").hidden = !this._multiEdit; - }, - - /** - * Initialize the panel - * @param aFor - * Either a places-itemId (of a bookmark, folder or a live bookmark), - * an array of itemIds (used for bulk tagging), or a URI object (in - * which case, the panel would be initialized in read-only mode). - * @param [optional] aInfo - * JS object which stores additional info for the panel - * initialization. The following properties may bet set: - * * hiddenRows (Strings array): list of rows to be hidden regardless - * of the item edited. Possible values: "title", "location", - * "description", "keyword", "loadInSidebar", "feedLocation", - * "siteLocation", folderPicker" - * * forceReadOnly - set this flag to initialize the panel to its - * read-only (view) mode even if the given item is editable. - */ - initPanel: function EIO_initPanel(aFor, aInfo) { // For sanity ensure that the implementer has uninited the panel before // trying to init it again, or we could end up leaking due to observers. - if (this._initialized) + if (this.initialized) this.uninitPanel(false); - var aItemIdList; - if (Array.isArray(aFor)) { - aItemIdList = aFor; - aFor = aItemIdList[0]; - } - else if (this._multiEdit) { - this._multiEdit = false; - this._tags = []; - this._uris = []; - this._allTags = []; - this._itemIds = []; - this._element("selectionCount").hidden = true; + let { itemId, itemGuid, isItem, + isURI, uri, title, + isBookmark, bulkTagging, uris, + visibleRows } = 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; } - this._folderMenuList = this._element("folderMenuList"); - this._folderTree = this._element("folderTree"); - - this._determineInfo(aInfo); - if (aFor instanceof Ci.nsIURI) { - this._itemId = -1; - this._uri = aFor; - this._readOnly = true; + if (showOrCollapse("locationRow", isURI, "location")) { + this._initLocationField(); + this._locationField.readOnly = !this._paneInfo.isItem; } - else { - this._itemId = aFor; - // We can't store information on invalid itemIds. - this._readOnly = this._readOnly || this._itemId == -1; - var containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId); - this._itemType = PlacesUtils.bookmarks.getItemType(this._itemId); - if (this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) { - this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId); - this._initTextField("keywordField", - PlacesUtils.bookmarks - .getKeywordForBookmark(this._itemId)); - this._element("loadInSidebarCheckbox").checked = - PlacesUtils.annotations.itemHasAnnotation(this._itemId, - PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); - } - else { - this._uri = null; - this._isLivemark = false; - PlacesUtils.livemarks.getLivemark({id: this._itemId }) - .then(aLivemark => { - this._isLivemark = true; - this._initTextField("feedLocationField", aLivemark.feedURI.spec, true); - this._initTextField("siteLocationField", aLivemark.siteURI ? aLivemark.siteURI.spec : "", true); - this._showHideRows(); - }, () => undefined); - } + if (showOrCollapse("descriptionRow", + this._paneInfo.isItem && !this.readOnly, + "description")) { + this._initDescriptionField(); + } - // folder picker + if (showOrCollapse("keywordRow", isBookmark, "keyword")) + this._initKeywordField(); + + // 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); - - // description field - this._initTextField("descriptionField", - PlacesUIUtils.getItemDescription(this._itemId)); } - if (this._itemId == -1 || - this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) { - this._isLivemark = false; - - this._initTextField("locationField", this._uri.spec); - if (!aItemIdList) { - var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); - this._initTextField("tagsField", tags, false); - } - else { - this._multiEdit = true; - this._allTags = []; - this._itemIds = aItemIdList; - for (var i = 0; i < aItemIdList.length; i++) { - if (aItemIdList[i] instanceof Ci.nsIURI) { - this._uris[i] = aItemIdList[i]; - this._itemIds[i] = -1; - } - else - this._uris[i] = PlacesUtils.bookmarks.getBookmarkURI(this._itemIds[i]); - this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]); - } - this._allTags = this._getCommonTags(); - this._initTextField("tagsField", this._allTags.join(", "), false); - this._element("itemsCountText").value = - PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", - this._itemIds.length, - [this._itemIds.length]); - } - - // tags selector - this._rebuildTagsSelectorList(); + // Selection count. + if (showOrCollapse("selectionCount", bulkTagging)) { + this._element("itemsCountText").value = + PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", + uris.length, + [uris.length]); } - // name picker - this._initNamePicker(); - - this._showHideRows(); - - // observe changes + // Observe changes. if (!this._observersAdded) { - // Single bookmarks observe any change. History entries and multiEdit - // observe only tags changes, through bookmarks. - if (this._itemId != -1 || this._uri || this._multiEdit) - PlacesUtils.bookmarks.addObserver(this, false); - - this._element("namePicker").addEventListener("blur", this); - this._element("locationField").addEventListener("blur", this); - this._element("tagsField").addEventListener("blur", this); - this._element("keywordField").addEventListener("blur", this); - this._element("descriptionField").addEventListener("blur", this); + PlacesUtils.bookmarks.addObserver(this, false); window.addEventListener("unload", this, false); this._observersAdded = true; } - - this._initialized = true; }, /** - * Finds tags that are in common among this._tags entries that track tags - * for each selected uri. - * The tags arrays should be kept up-to-date for this to work properly. - * - * @return array of common tags for the selected uris. + * Finds tags that are in common among this._currentInfo.uris; */ - _getCommonTags: function() { - return this._tags[0].filter( - function (aTag) this._tags.every( - function (aTags) aTags.indexOf(aTag) != -1 - ), this - ); + _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.indexOf(tag) == -1) { + commonTags.delete(tag) + if (commonTags.size == 0) + return this._paneInfo.cachedCommonTags = []; + } + } + } + return this._paneInfo._cachedCommonTags = [...commonTags]; }, - _initTextField: function(aTextFieldId, aValue, aReadOnly) { - var field = this._element(aTextFieldId); - field.readOnly = aReadOnly !== undefined ? aReadOnly : this._readOnly; + _initTextField(aElement, aValue) { + if (aElement.value != aValue) { + aElement.value = aValue; - if (field.value != aValue) { - field.value = aValue; - - // clear the undo stack - var editor = field.editor; + // Clear the undo stack + let editor = aElement.editor; if (editor) editor.transactionManager.clear(); } @@ -259,8 +279,7 @@ var gEditItemOverlay = { * The identifier of the bookmarks folder. * @return the new menu item. */ - _appendFolderItemToMenupopup: - function EIO__appendFolderItemToMenuList(aMenupopup, aFolderId) { + _appendFolderItemToMenupopup(aMenupopup, aFolderId) { // First make sure the folders-separator is visible this._element("foldersSeparator").hidden = false; @@ -339,144 +358,136 @@ var gEditItemOverlay = { this._folderMenuList.disabled = this._readOnly; }, - QueryInterface: function EIO_QueryInterface(aIID) { - if (aIID.equals(Ci.nsIDOMEventListener) || - aIID.equals(Ci.nsINavBookmarkObserver) || - aIID.equals(Ci.nsISupports)) - return this; + QueryInterface: + XPCOMUtils.generateQI([Components.interfaces.nsIDOMEventListener, + Components.interfaces.nsINavBookmarkObserver]), - throw Cr.NS_ERROR_NO_INTERFACE; - }, + _element(aID) document.getElementById("editBMPanel_" + aID), - _element: function EIO__element(aID) { - return document.getElementById("editBMPanel_" + aID); - }, - - _getItemStaticTitle: function EIO__getItemStaticTitle() { - if (this._titleOverride) - return this._titleOverride; - - let title = ""; - if (this._itemId == -1) { - title = PlacesUtils.history.getPageTitle(this._uri); - } - else { - title = PlacesUtils.bookmarks.getItemTitle(this._itemId); - } - return title; - }, - - _initNamePicker: function EIO_initNamePicker() { - var namePicker = this._element("namePicker"); - namePicker.value = this._getItemStaticTitle(); - namePicker.readOnly = this._readOnly; - - // clear the undo stack - var editor = namePicker.editor; - if (editor) - editor.transactionManager.clear(); - }, - - uninitPanel: function EIO_uninitPanel(aHideCollapsibleElements) { + uninitPanel(aHideCollapsibleElements) { if (aHideCollapsibleElements) { - // hide the folder tree if it was previously visible + // 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 + // Hide the tag selector if it was previously visible. var tagsSelectorRow = this._element("tagsSelectorRow"); if (!tagsSelectorRow.collapsed) this.toggleTagsSelector(); } if (this._observersAdded) { - if (this._itemId != -1 || this._uri || this._multiEdit) - PlacesUtils.bookmarks.removeObserver(this); - - this._element("namePicker").removeEventListener("blur", this); - this._element("locationField").removeEventListener("blur", this); - this._element("tagsField").removeEventListener("blur", this); - this._element("keywordField").removeEventListener("blur", this); - this._element("descriptionField").removeEventListener("blur", this); - + PlacesUtils.bookmarks.removeObserver(this); this._observersAdded = false; } - this._itemId = -1; - this._uri = null; - this._uris = []; - this._tags = []; - this._allTags = []; - this._itemIds = []; - this._multiEdit = false; + this._setPaneInfo(null); this._firstEditedField = ""; - this._initialized = false; - this._titleOverride = ""; - this._readOnly = false; }, - onTagsFieldBlur: function EIO_onTagsFieldBlur() { - if (this._updateTags()) // if anything has changed - this._mayUpdateFirstEditField("tagsField"); - }, - - _updateTags: function EIO__updateTags() { - if (this._multiEdit) - return this._updateMultipleTagsForItems(); - return this._updateSingleTagForItem(); - }, - - _updateSingleTagForItem: function EIO__updateSingleTagForItem() { - var currentTags = PlacesUtils.tagging.getTagsForURI(this._uri); - var tags = this._getTagsArrayFromTagField(); - if (tags.length > 0 || currentTags.length > 0) { - var tagsToRemove = []; - var tagsToAdd = []; - var txns = []; - for (var i = 0; i < currentTags.length; i++) { - if (tags.indexOf(currentTags[i]) == -1) - tagsToRemove.push(currentTags[i]); - } - for (var i = 0; i < tags.length; i++) { - if (currentTags.indexOf(tags[i]) == -1) - tagsToAdd.push(tags[i]); - } - - if (tagsToRemove.length > 0) { - let untagTxn = new PlacesUntagURITransaction(this._uri, tagsToRemove); - txns.push(untagTxn); - } - if (tagsToAdd.length > 0) { - let tagTxn = new PlacesTagURITransaction(this._uri, tagsToAdd); - txns.push(tagTxn); - } - - if (txns.length > 0) { - let aggregate = new PlacesAggregatedTransaction("Update tags", txns); - PlacesUtils.transactionManager.doTransaction(aggregate); - - // Ensure the tagsField is in sync, clean it up from empty tags - var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); - this._initTextField("tagsField", tags, false); - return true; - } + onTagsFieldChange() { + if (!this.readOnly) { + this._updateTags().then( + anyChanges => { + if (anyChanges) + this._mayUpdateFirstEditField("tagsField"); + }, Components.utils.reportError); } - return false; }, - /** - * Stores the first-edit field for this dialog, if the passed-in field - * is indeed the first edited field - * @param aNewField - * the id of the field that may be set (without the "editBMPanel_" - * prefix) - */ - _mayUpdateFirstEditField: function EIO__mayUpdateFirstEditField(aNewField) { + /** + * 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.indexOf(t) == -1); + let newTags = inputTags.filter(t => aCurrentTags.indexOf(t) == -1); + 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; + + // Ensure the tagsField is in sync, clean it up from empty tags + currentTags = this._paneInfo.bulkTagging ? + this._getCommonTags() : + PlacesUtils.tagging.getTagsForURI(this._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._multiEdit || this._firstEditedField) + if (this._paneInfo.bulkTagging || this._firstEditedField) return; this._firstEditedField = aNewField; @@ -487,124 +498,116 @@ var gEditItemOverlay = { prefs.setCharPref("browser.bookmarks.editDialog.firstEditField", aNewField); }, - _updateMultipleTagsForItems: function EIO__updateMultipleTagsForItems() { - var tags = this._getTagsArrayFromTagField(); - if (tags.length > 0 || this._allTags.length > 0) { - var tagsToRemove = []; - var tagsToAdd = []; - var txns = []; - for (var i = 0; i < this._allTags.length; i++) { - if (tags.indexOf(this._allTags[i]) == -1) - tagsToRemove.push(this._allTags[i]); - } - for (var i = 0; i < this._tags.length; i++) { - tagsToAdd[i] = []; - for (var j = 0; j < tags.length; j++) { - if (this._tags[i].indexOf(tags[j]) == -1) - tagsToAdd[i].push(tags[j]); - } - } - - if (tagsToAdd.length > 0) { - for (let i = 0; i < this._uris.length; i++) { - if (tagsToAdd[i].length > 0) { - let tagTxn = new PlacesTagURITransaction(this._uris[i], - tagsToAdd[i]); - txns.push(tagTxn); - } - } - } - if (tagsToRemove.length > 0) { - for (let i = 0; i < this._uris.length; i++) { - let untagTxn = new PlacesUntagURITransaction(this._uris[i], - tagsToRemove); - txns.push(untagTxn); - } - } - - if (txns.length > 0) { - let aggregate = new PlacesAggregatedTransaction("Update tags", txns); - PlacesUtils.transactionManager.doTransaction(aggregate); - - this._allTags = tags; - this._tags = []; - for (let i = 0; i < this._uris.length; i++) { - this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]); - } - - // Ensure the tagsField is in sync, clean it up from empty tags - this._initTextField("tagsField", tags, false); - return true; - } - } - return false; - }, - - onNamePickerBlur: function EIO_onNamePickerBlur() { - if (this._itemId == -1) + onNamePickerChange() { + if (this.readOnly || !this._paneInfo.isItem) return; - var namePicker = this._element("namePicker") - // Here we update either the item title or its cached static title - var newTitle = namePicker.value; + let newTitle = this._namePicker.value; if (!newTitle && - PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) == PlacesUtils.tagsFolderId) { + PlacesUtils.bookmarks.getFolderIdForItem(itemId) == PlacesUtils.tagsFolderId) { // We don't allow setting an empty title for a tag, restore the old one. this._initNamePicker(); } - else if (this._getItemStaticTitle() != newTitle) { + else { this._mayUpdateFirstEditField("namePicker"); - let txn = new PlacesEditItemTitleTransaction(this._itemId, newTitle); - PlacesUtils.transactionManager.doTransaction(txn); + if (!PlacesUIUtils.useAsyncTransactions) { + let txn = new PlacesEditItemTitleTransaction(this._paneInfo.itemId, + newTitle); + PlacesUtils.transactionManager.doTransaction(txn); + return; + } + let guid = this._paneInfo.itemGuid; + PlacesTransactions.EditTitle({ guid, title: newTitle }) + .transact().catch(Components.utils.reportError); } }, - onDescriptionFieldBlur: function EIO_onDescriptionFieldBlur() { - var description = this._element("descriptionField").value; + onDescriptionFieldChange() { + if (this.readOnly || !this._paneInfo.isItem) + return; + + let itemId = this._paneInfo.itemId; + let description = this._element("descriptionField").value; if (description != PlacesUIUtils.getItemDescription(this._itemId)) { - var annoObj = { name : PlacesUIUtils.DESCRIPTION_ANNO, - type : Ci.nsIAnnotationService.TYPE_STRING, - flags : 0, - value : description, - expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; - var txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj); - PlacesUtils.transactionManager.doTransaction(txn); + 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); } }, - onLocationFieldBlur: function EIO_onLocationFieldBlur() { - var uri; + onLocationFieldChange() { + if (this.readOnly || !this._paneInfo.isBookmark) + return; + + let newURI; try { - uri = PlacesUIUtils.createFixedURI(this._element("locationField").value); + newURI = PlacesUIUtils.createFixedURI(this._locationField.value); + } + catch(ex) { + // TODO: Bug 1089141 - Provide some feedback about the invalid url. + return; } - catch(ex) { return; } - if (!this._uri.equals(uri)) { - var txn = new PlacesEditBookmarkURITransaction(this._itemId, uri); + if (this._paneInfo.uri.equals(newURI)) + return; + + if (!PlacesUIUtils.useAsyncTransactions) { + let itemId = this._paneInfo.itemId; + let txn = new PlacesEditBookmarkURITransaction(this._itemId, newURI); PlacesUtils.transactionManager.doTransaction(txn); - this._uri = uri; + return; } + let guid = this._paneInfo.itemGuid; + PlacesTransactions.EditUrl({ guid, url: newURI }) + .transact().catch(Components.utils.reportError); }, - onKeywordFieldBlur: function EIO_onKeywordFieldBlur() { - var keyword = this._element("keywordField").value; - if (keyword != PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId)) { - var txn = new PlacesEditBookmarkKeywordTransaction(this._itemId, keyword); + onKeywordFieldChange() { + if (this.readOnly || !this._paneInfo.isBookmark) + return; + + let itemId = this._paneInfo.itemId; + let newKeyword = this._keywordField.value; + if (!PlacesUIUtils.useAsyncTransactions) { + let txn = new PlacesEditBookmarkKeywordTransaction(itemId, newKeyword); PlacesUtils.transactionManager.doTransaction(txn); + return; } + let guid = this._paneInfo.itemGuid; + PlacesTransactions.EditKeyword({ guid, keyword: newKeyword }) + .transact().catch(Components.utils.reportError); }, - onLoadInSidebarCheckboxCommand: - function EIO_onLoadInSidebarCheckboxCommand() { - let annoObj = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO }; - if (this._element("loadInSidebarCheckbox").checked) - annoObj.value = true; - let txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj); - PlacesUtils.transactionManager.doTransaction(txn); + 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: function EIO_toggleFolderTreeVisibility() { + toggleFolderTreeVisibility() { var expander = this._element("foldersExpander"); var folderTreeRow = this._element("folderTreeRow"); if (!folderTreeRow.collapsed) { @@ -637,8 +640,7 @@ var gEditItemOverlay = { } }, - _getFolderIdFromMenuList: - function EIO__getFolderIdFromMenuList() { + _getFolderIdFromMenuList() { var selectedItem = this._folderMenuList.selectedItem; NS_ASSERT("folderId" in selectedItem, "Invalid menuitem in the folders-menulist"); @@ -653,24 +655,21 @@ var gEditItemOverlay = { * @param aFolderId * The identifier of the bookmarks folder. */ - _getFolderMenuItem: - function EIO__getFolderMenuItem(aFolderId) { - var menupopup = this._folderMenuList.menupopup; - - for (let i = 0; i < menupopup.childNodes.length; i++) { - if ("folderId" in menupopup.childNodes[i] && - menupopup.childNodes[i].folderId == aFolderId) - return menupopup.childNodes[i]; - } + _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); + menupopup.removeChild(menuPopup.lastChild); - return this._appendFolderItemToMenupopup(menupopup, aFolderId); + return this._appendFolderItemToMenupopup(menuPopup, aFolderId); }, - onFolderMenuListCommand: function EIO_onFolderMenuListCommand(aEvent) { + onFolderMenuListCommand(aEvent) { // Set a selectedIndex attribute to show special icons this._folderMenuList.setAttribute("selectedIndex", this._folderMenuList.selectedIndex); @@ -678,8 +677,8 @@ var gEditItemOverlay = { if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") { // reset the selection back to where it was and expand the tree // (this menu-item is hidden when the tree is already visible - var container = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId); - var item = this._getFolderMenuItem(container); + let containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId); + let item = this._getFolderMenuItem(containerId); this._folderMenuList.selectedItem = item; // XXXmano HACK: setTimeout 100, otherwise focus goes back to the // menulist right away @@ -688,19 +687,30 @@ var gEditItemOverlay = { } // Move the item - var container = this._getFolderIdFromMenuList(); - if (PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) != container) { - var txn = new PlacesMoveItemTransaction(this._itemId, - container, - PlacesUtils.bookmarks.DEFAULT_INDEX); - PlacesUtils.transactionManager.doTransaction(txn); + let containerId = this._getFolderIdFromMenuList(); + if (PlacesUtils.bookmarks.getFolderIdForItem(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._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 (container != PlacesUtils.unfiledBookmarksFolderId && - container != PlacesUtils.toolbarFolderId && - container != PlacesUtils.bookmarksMenuFolderId) - this._markFolderAsRecentlyUsed(container); + if (containerId != PlacesUtils.unfiledBookmarksFolderId && + containerId != PlacesUtils.toolbarFolderId && + containerId != PlacesUtils.bookmarksMenuFolderId) { + this._markFolderAsRecentlyUsed(container) + .catch(Components.utils.reportError); + } } // Update folder-tree selection @@ -713,7 +723,7 @@ var gEditItemOverlay = { } }, - onFolderTreeSelect: function EIO_onFolderTreeSelect() { + onFolderTreeSelect() { var selectedNode = this._folderTree.selectedNode; // Disable the "New Folder" button if we cannot create a new folder @@ -732,26 +742,48 @@ var gEditItemOverlay = { folderItem.doCommand(); }, - _markFolderAsRecentlyUsed: - function EIO__markFolderAsRecentlyUsed(aFolderId) { - var txns = []; + _markFolderAsRecentlyUsed: Task.async(function* (aFolderId) { + if (!PlacesUIUtils.useAsyncTransactions) { + let txns = []; - // Expire old unused recent folders - var anno = this._getLastUsedAnnotationObject(false); - while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) { - var folderId = this._recentFolders.pop().folderId; - let annoTxn = new PlacesSetItemAnnotationTransaction(folderId, anno); + // 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, anno); + txns.push(annoTxn); + } + + // Mark folder as recently used + annotation = this._getLastUsedAnnotationObject(true); + let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno); txns.push(annoTxn); + + let aggregate = + new PlacesAggregatedTransaction("Update last used folders", txns); + PlacesUtils.transactionManager.doTransaction(aggregate); + 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 - anno = this._getLastUsedAnnotationObject(true); - let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno); - txns.push(annoTxn); - - let aggregate = new PlacesAggregatedTransaction("Update last used folders", txns); - PlacesUtils.transactionManager.doTransaction(aggregate); - }, + 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 @@ -762,20 +794,14 @@ var gEditItemOverlay = { * @returns an object representing the annotation which could then be used * with the transaction manager. */ - _getLastUsedAnnotationObject: - function EIO__getLastUsedAnnotationObject(aLastUsed) { - var anno = { name: LAST_USED_ANNO, - type: Ci.nsIAnnotationService.TYPE_INT32, - flags: 0, - value: aLastUsed ? new Date().getTime() : null, - expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; - - return anno; + _getLastUsedAnnotationObject(aLastUsed) { + return { name: LAST_USED_ANNO, + value: aLastUsed ? new Date().getTime() : null }; }, - _rebuildTagsSelectorList: function EIO__rebuildTagsSelectorList() { - var tagsSelector = this._element("tagsSelector"); - var tagsSelectorRow = this._element("tagsSelectorRow"); + _rebuildTagsSelectorList: Task.async(function* () { + let tagsSelector = this._element("tagsSelector"); + let tagsSelectorRow = this._element("tagsSelectorRow"); if (tagsSelectorRow.collapsed) return; @@ -785,14 +811,14 @@ var gEditItemOverlay = { let selectedTag = selectedIndex >= 0 ? tagsSelector.selectedItem.label : null; - while (tagsSelector.hasChildNodes()) + while (tagsSelector.hasChildNodes()) { tagsSelector.removeChild(tagsSelector.lastChild); + } - var tagsInField = this._getTagsArrayFromTagField(); - var allTags = PlacesUtils.tagging.allTags; - for (var i = 0; i < allTags.length; i++) { - var tag = allTags[i]; - var elt = document.createElement("listitem"); + 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.indexOf(tag) != -1) @@ -814,9 +840,9 @@ var gEditItemOverlay = { tagsSelector.selectedIndex = selectedIndex; tagsSelector.ensureIndexIsVisible(selectedIndex); } - }, + }), - toggleTagsSelector: function EIO_toggleTagsSelector() { + toggleTagsSelector: Task.async(function* () { var tagsSelector = this._element("tagsSelector"); var tagsSelectorRow = this._element("tagsSelectorRow"); var expander = this._element("tagsSelectorExpander"); @@ -825,7 +851,7 @@ var gEditItemOverlay = { expander.setAttribute("tooltiptext", expander.getAttribute("tooltiptextup")); tagsSelectorRow.collapsed = false; - this._rebuildTagsSelectorList(); + yield this._rebuildTagsSelectorList(); // This is a no-op if we've added the listener. tagsSelector.addEventListener("CheckboxStateChange", this, false); @@ -836,204 +862,209 @@ var gEditItemOverlay = { expander.getAttribute("tooltiptextdown")); tagsSelectorRow.collapsed = true; } - }, + }), /** * Splits "tagsField" element value, returning an array of valid tag strings. * * @return Array of tag strings found in the field value. */ - _getTagsArrayFromTagField: function EIO__getTagsArrayFromTagField() { + _getTagsArrayFromTagsInputField() { let tags = this._element("tagsField").value; return tags.trim() .split(/\s*,\s*/) // Split on commas and remove spaces. .filter(function (tag) tag.length > 0); // Kill empty tags. }, - newFolder: function EIO_newFolder() { - var ip = this._folderTree.insertionPoint; + 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); + ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + Ci.nsITreeView.DROP_ON); } // XXXmano: add a separate "New Folder" string at some point... - var defaultLabel = this._element("newFolderButton").label; - var txn = new PlacesCreateFolderTransaction(defaultLabel, ip.itemId, ip.index); - PlacesUtils.transactionManager.doTransaction(txn); + 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([this._lastNewItem]); this._folderTree.startEditing(this._folderTree.view.selection.currentIndex, this._folderTree.columns.getFirstColumn()); - }, + }), // nsIDOMEventListener - handleEvent: function EIO_nsIDOMEventListener(aEvent) { + handleEvent(aEvent) { switch (aEvent.type) { case "CheckboxStateChange": // Update the tags field when items are checked/unchecked in the listbox - var tags = this._getTagsArrayFromTagField(); + let tags = this._getTagsArrayFromTagsInputField(); + let tagCheckbox = aEvent.target; - if (aEvent.target.checked) { - if (tags.indexOf(aEvent.target.label) == -1) - tags.push(aEvent.target.label); + 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 { - var indexOfItem = tags.indexOf(aEvent.target.label); - if (indexOfItem != -1) - tags.splice(indexOfItem, 1); + if (curTagIndex != -1) + tags.splice(curTagIndex, 1); } this._element("tagsField").value = tags.join(", "); this._updateTags(); break; - case "blur": - let replaceFn = (str, firstLetter) => firstLetter.toUpperCase(); - let nodeName = aEvent.target.id.replace(/editBMPanel_(\w)/, replaceFn); - this["on" + nodeName + "Blur"](); - break; case "unload": this.uninitPanel(false); break; } }, - // nsINavBookmarkObserver - onItemChanged: function EIO_onItemChanged(aItemId, aProperty, - aIsAnnotationProperty, aValue, - aLastModified, aItemType) { - if (aProperty == "tags") { - // Tags case is special, since they should be updated if either: - // - the notification is for the edited bookmark - // - the notification is for the edited history entry - // - the notification is for one of edited uris - let shouldUpdateTagsField = this._itemId == aItemId; - if (this._itemId == -1 || this._multiEdit) { - // Check if the changed uri is part of the modified ones. + _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); - let uris = this._multiEdit ? this._uris : [this._uri]; - uris.forEach(function (aURI, aIndex) { - if (aURI.equals(changedURI)) { - shouldUpdateTagsField = true; - if (this._multiEdit) { - this._tags[aIndex] = PlacesUtils.tagging.getTagsForURI(this._uris[aIndex]); - } - } - }, this); + updateTagsField = changedURI.equals(paneInfo.uri); } - - if (shouldUpdateTagsField) { - if (this._multiEdit) { - this._allTags = this._getCommonTags(); - this._initTextField("tagsField", this._allTags.join(", "), false); - } - else { - let tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); - this._initTextField("tagsField", tags, false); - } + } + else if (paneInfo.bulkTagging) { + let changedURI = PlacesUtils.bookmarks.getBookmarkURI(aItemId); + if (paneInfo.uris.some(uri => uri.equals(changedURI))) { + updateTagsField = true; + delete this._paneInfo._cachedCommonTags; } - - // Any tags change should be reflected in the tags selector. - this._rebuildTagsSelectorList(); - return; + } + else { + throw new Error("_onTagsChange called unexpectedly"); } - if (this._itemId != aItemId) { - if (aProperty == "title") { - // If the title of a folder which is listed within the folders - // menulist has been changed, we need to update the label of its - // representing element. - var menupopup = this._folderMenuList.menupopup; - for (let i = 0; i < menupopup.childNodes.length; i++) { - if ("folderId" in menupopup.childNodes[i] && - menupopup.childNodes[i].folderId == aItemId) { - menupopup.childNodes[i].label = aValue; - break; - } + 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); + } + 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; } } - - return; } + }, + + // nsINavBookmarkObserver + onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aValue, + aLastModified, aItemType) { + if (aProperty == "tags" && this._paneInfo.visibleRows.has("tagsRow")) + this._onTagsChange(aItemId); + else if (this._paneInfo.isItem && aProperty == "title") + this._onItemTitleChange(aItemId, aValue); + else (!this._paneInfo.isItem || this._paneInfo.itemId != aItemId) + return; switch (aProperty) { - case "title": - var namePicker = this._element("namePicker"); - if (namePicker.value != aValue) { - namePicker.value = aValue; - // clear undo stack - namePicker.editor.transactionManager.clear(); - } - break; case "uri": - var locationField = this._element("locationField"); - if (locationField.value != aValue) { - this._uri = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService). - newURI(aValue, null, null); - this._initTextField("locationField", this._uri.spec); - this._initNamePicker(); - this._initTextField("tagsField", - PlacesUtils.tagging - .getTagsForURI(this._uri).join(", "), - false); - this._rebuildTagsSelectorList(); + 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": - this._initTextField("keywordField", - PlacesUtils.bookmarks - .getKeywordForBookmark(this._itemId)); + if (this._paneInfo.visibleRows.has("keywordRow")) + this._initKeywordField(aValue); break; case PlacesUIUtils.DESCRIPTION_ANNO: - this._initTextField("descriptionField", - PlacesUIUtils.getItemDescription(this._itemId)); + if (this._paneInfo.visibleRows.has("descriptionRow")) + this._initDescriptionField(); break; case PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO: - this._element("loadInSidebarCheckbox").checked = - PlacesUtils.annotations.itemHasAnnotation(this._itemId, - PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); - break; - case PlacesUtils.LMANNO_FEEDURI: - let feedURISpec = - PlacesUtils.annotations.getItemAnnotation(this._itemId, - PlacesUtils.LMANNO_FEEDURI); - this._initTextField("feedLocationField", feedURISpec, true); - break; - case PlacesUtils.LMANNO_SITEURI: - let siteURISpec = ""; - try { - siteURISpec = - PlacesUtils.annotations.getItemAnnotation(this._itemId, - PlacesUtils.LMANNO_SITEURI); - } catch (ex) {} - this._initTextField("siteLocationField", siteURISpec, true); + if (this._paneInfo.visibleRows.has("loadInSidebarCheckbox")) + this._initLoadInSidebar(); break; } }, - onItemMoved: function EIO_onItemMoved(aItemId, aOldParent, aOldIndex, - aNewParent, aNewIndex, aItemType) { - if (aItemId != this._itemId || - aNewParent == this._getFolderIdFromMenuList()) + onItemMoved(aItemId, aOldParent, aOldIndex, + aNewParent, aNewIndex, aItemType) { + if (!this._paneInfo.isItem || + !this._paneInfo.visibleRows.has("folderPicker") || + this._paneInfo.itemId != aItemOd || + aNewParent == this._getFolderIdFromMenuList()) { return; + } - var folderItem = this._getFolderMenuItem(aNewParent); - - // just setting selectItem _does not_ trigger oncommand, so we don't - // recurse - this._folderMenuList.selectedItem = folderItem; + // Just setting selectItem _does not_ trigger oncommand, so we don't + // recurse. + this._folderMenuList.selectedItem = this._getFolderMenuItem(aNewParent); }, - onItemAdded: function EIO_onItemAdded(aItemId, aParentId, aIndex, aItemType, - aURI) { + onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI) { this._lastNewItem = aItemId; }, - onItemRemoved: function() { }, - onBeginUpdateBatch: function() { }, - onEndUpdateBatch: function() { }, - onItemVisited: function() { }, + 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 index 196369dd2e..4d5fa8c99f 100644 --- a/browser/components/places/content/editBookmarkOverlay.xul +++ b/browser/components/places/content/editBookmarkOverlay.xul @@ -32,10 +32,9 @@