summaryrefslogtreecommitdiffstats
path: root/application/basilisk/components/places/content/editBookmarkOverlay.js
diff options
context:
space:
mode:
Diffstat (limited to 'application/basilisk/components/places/content/editBookmarkOverlay.js')
-rw-r--r--application/basilisk/components/places/content/editBookmarkOverlay.js1196
1 files changed, 1196 insertions, 0 deletions
diff --git a/application/basilisk/components/places/content/editBookmarkOverlay.js b/application/basilisk/components/places/content/editBookmarkOverlay.js
new file mode 100644
index 000000000..d59f5c764
--- /dev/null
+++ b/application/basilisk/components/places/content/editBookmarkOverlay.js
@@ -0,0 +1,1196 @@
+/* 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;
+ let onPanelReady = aInitInfo.onPanelReady;
+
+ return this._paneInfo = { itemId, itemGuid, isItem,
+ isURI, uri, title,
+ isBookmark, isFolderShortcut, isParentReadOnly,
+ bulkTagging, uris,
+ visibleRows, postData, isTag, focusedElement,
+ onPanelReady };
+ },
+
+ 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,
+ onPanelReady } = 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;
+ }
+
+ let focusElement = () => {
+ // 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.
+ // Note: If focusedElement === "preferred", this file expects gPrefService
+ // to be defined in the global scope.
+ let elt;
+ if (focusedElement === "preferred") {
+ /* eslint-disable no-undef */
+ elt = this._element(gPrefService.getCharPref("browser.bookmarks.editDialog.firstEditField"));
+ /* eslint-enable no-undef */
+ } else if (focusedElement === "first") {
+ elt = document.querySelector("textbox:not([collapsed=true])");
+ }
+ if (elt) {
+ elt.focus();
+ elt.select();
+ }
+ };
+
+ if (onPanelReady) {
+ onPanelReady(focusElement);
+ } else {
+ focusElement();
+ }
+ },
+
+ /**
+ * 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 editor's undo stack
+ let transactionManager;
+ try {
+ transactionManager = aElement.editor.transactionManager;
+ } catch (e) {
+ // When retrieving the transaction manager, editor may be null resulting
+ // in a TypeError. Additionally, the transaction manager may not
+ // exist yet, which causes access to it to throw NS_ERROR_FAILURE.
+ // In either event, the transaction manager doesn't exist it, so we
+ // don't need to worry about clearing it.
+ if (!(e instanceof TypeError) && e.result != Cr.NS_ERROR_FAILURE) {
+ throw e;
+ }
+ }
+ if (transactionManager) {
+ transactionManager.clear();
+ }
+ }
+ },
+
+ /**
+ * 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));
+}