summaryrefslogtreecommitdiffstats
path: root/components/places/content/editBookmarkOverlay.js
diff options
context:
space:
mode:
Diffstat (limited to 'components/places/content/editBookmarkOverlay.js')
-rw-r--r--components/places/content/editBookmarkOverlay.js1063
1 files changed, 1063 insertions, 0 deletions
diff --git a/components/places/content/editBookmarkOverlay.js b/components/places/content/editBookmarkOverlay.js
new file mode 100644
index 0000000..69d7d32
--- /dev/null
+++ b/components/places/content/editBookmarkOverlay.js
@@ -0,0 +1,1063 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed";
+const MAX_FOLDER_ITEM_IN_MENU_LIST = 5;
+
+var gEditItemOverlay = {
+ _uri: null,
+ _itemId: -1,
+ _itemIds: [],
+ _uris: [],
+ _tags: [],
+ _allTags: [],
+ _keyword: null,
+ _multiEdit: false,
+ _itemType: -1,
+ _readOnly: false,
+ _hiddenRows: [],
+ _onPanelReady: false,
+ _observersAdded: false,
+ _staticFoldersListBuilt: false,
+ _initialized: false,
+ _titleOverride: "",
+
+ // the first field which was edited after this panel was initialized for
+ // a certain item
+ _firstEditedField: "",
+
+ get itemId() {
+ return this._itemId;
+ },
+
+ get uri() {
+ return this._uri;
+ },
+
+ get multiEdit() {
+ return this._multiEdit;
+ },
+
+ /**
+ * Determines the initial data for the item edited or added by this dialog
+ */
+ _determineInfo: function EIO__determineInfo(aInfo) {
+ // hidden rows
+ if (aInfo && aInfo.hiddenRows)
+ this._hiddenRows = aInfo.hiddenRows;
+ else
+ this._hiddenRows.splice(0, this._hiddenRows.length);
+ // force-read-only
+ this._readOnly = aInfo && aInfo.forceReadOnly;
+ this._titleOverride = aInfo && aInfo.titleOverride ? aInfo.titleOverride
+ : "";
+ this._onPanelReady = aInfo && aInfo.onPanelReady;
+ },
+
+ _showHideRows: function EIO__showHideRows() {
+ var isBookmark = this._itemId != -1 &&
+ this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK;
+ var isQuery = false;
+ if (this._uri)
+ isQuery = this._uri.schemeIs("place");
+
+ this._element("nameRow").collapsed = this._hiddenRows.indexOf("name") != -1;
+ this._element("folderRow").collapsed =
+ this._hiddenRows.indexOf("folderPicker") != -1 || this._readOnly;
+ this._element("tagsRow").collapsed = !this._uri ||
+ this._hiddenRows.indexOf("tags") != -1 || isQuery;
+ // Collapse the tag selector if the item does not accept tags.
+ if (!this._element("tagsSelectorRow").collapsed &&
+ this._element("tagsRow").collapsed)
+ this.toggleTagsSelector();
+ this._element("descriptionRow").collapsed =
+ this._hiddenRows.indexOf("description") != -1 || this._readOnly;
+ this._element("keywordRow").collapsed = !isBookmark || this._readOnly ||
+ this._hiddenRows.indexOf("keyword") != -1 || isQuery;
+ this._element("locationRow").collapsed = !(this._uri && !isQuery) ||
+ this._hiddenRows.indexOf("location") != -1;
+ this._element("loadInSidebarCheckbox").collapsed = !isBookmark || isQuery ||
+ this._readOnly || this._hiddenRows.indexOf("loadInSidebar") != -1;
+ this._element("feedLocationRow").collapsed = !this._isLivemark ||
+ this._hiddenRows.indexOf("feedLocation") != -1;
+ this._element("siteLocationRow").collapsed = !this._isLivemark ||
+ this._hiddenRows.indexOf("siteLocation") != -1;
+ this._element("selectionCount").hidden = !this._multiEdit;
+ },
+
+ /**
+ * Initialize the panel
+ * @param aFor
+ * Either a places-itemId (of a bookmark, folder or a live bookmark),
+ * an array of itemIds (used for bulk tagging), or a URI object (in
+ * which case, the panel would be initialized in read-only mode).
+ * @param [optional] aInfo
+ * JS object which stores additional info for the panel
+ * initialization. The following properties may bet set:
+ * * hiddenRows (Strings array): list of rows to be hidden regardless
+ * of the item edited. Possible values: "title", "location",
+ * "description", "keyword", "loadInSidebar", "feedLocation",
+ * "siteLocation", folderPicker"
+ * * forceReadOnly - set this flag to initialize the panel to its
+ * read-only (view) mode even if the given item is editable.
+ */
+ initPanel: function EIO_initPanel(aFor, aInfo) {
+ // For sanity ensure that the implementer has uninited the panel before
+ // trying to init it again, or we could end up leaking due to observers.
+ if (this._initialized)
+ this.uninitPanel(false);
+
+ var aItemIdList;
+ if (Array.isArray(aFor)) {
+ aItemIdList = aFor;
+ aFor = aItemIdList[0];
+ }
+ else if (this._multiEdit) {
+ this._multiEdit = false;
+ this._tags = [];
+ this._uris = [];
+ this._allTags = [];
+ this._itemIds = [];
+ this._element("selectionCount").hidden = true;
+ }
+
+ this._folderMenuList = this._element("folderMenuList");
+ this._folderTree = this._element("folderTree");
+
+ this._determineInfo(aInfo);
+ if (aFor instanceof Ci.nsIURI) {
+ this._itemId = -1;
+ this._uri = aFor;
+ this._readOnly = true;
+ }
+ else {
+ this._itemId = aFor;
+ // We can't store information on invalid itemIds.
+ this._readOnly = this._readOnly || this._itemId == -1;
+
+ var containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
+ this._itemType = PlacesUtils.bookmarks.getItemType(this._itemId);
+ if (this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
+ this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
+ this._keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId);
+ this._initTextField("keywordField", this._keyword);
+ this._element("loadInSidebarCheckbox").checked =
+ PlacesUtils.annotations.itemHasAnnotation(this._itemId,
+ PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
+ }
+ else {
+ this._uri = null;
+ this._isLivemark = false;
+ PlacesUtils.livemarks.getLivemark({id: this._itemId })
+ .then(aLivemark => {
+ this._isLivemark = true;
+ this._initTextField("feedLocationField", aLivemark.feedURI.spec, true);
+ this._initTextField("siteLocationField", aLivemark.siteURI ? aLivemark.siteURI.spec : "", true);
+ this._showHideRows();
+ }, () => undefined);
+ }
+
+ // folder picker
+ this._initFolderMenuList(containerId);
+
+ // description field
+ this._initTextField("descriptionField",
+ PlacesUIUtils.getItemDescription(this._itemId));
+ }
+
+ if (this._itemId == -1 ||
+ this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
+ this._isLivemark = false;
+
+ this._initTextField("locationField", this._uri.spec);
+ if (!aItemIdList) {
+ var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", ");
+ this._initTextField("tagsField", tags, false);
+ }
+ else {
+ this._multiEdit = true;
+ this._allTags = [];
+ this._itemIds = aItemIdList;
+ for (var i = 0; i < aItemIdList.length; i++) {
+ if (aItemIdList[i] instanceof Ci.nsIURI) {
+ this._uris[i] = aItemIdList[i];
+ this._itemIds[i] = -1;
+ }
+ else
+ this._uris[i] = PlacesUtils.bookmarks.getBookmarkURI(this._itemIds[i]);
+ this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]);
+ }
+ this._allTags = this._getCommonTags();
+ this._initTextField("tagsField", this._allTags.join(", "), false);
+ this._element("itemsCountText").value =
+ PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
+ this._itemIds.length,
+ [this._itemIds.length]);
+ }
+
+ // tags selector
+ this._rebuildTagsSelectorList();
+ }
+
+ // name picker
+ this._initNamePicker();
+
+ this._showHideRows();
+
+ // observe changes
+ if (!this._observersAdded) {
+ // Single bookmarks observe any change. History entries and multiEdit
+ // observe only tags changes, through bookmarks.
+ if (this._itemId != -1 || this._uri || this._multiEdit)
+ PlacesUtils.bookmarks.addObserver(this, false);
+
+ this._element("namePicker").addEventListener("blur", this);
+ this._element("locationField").addEventListener("blur", this);
+ this._element("tagsField").addEventListener("blur", this);
+ this._element("keywordField").addEventListener("blur", this);
+ this._element("descriptionField").addEventListener("blur", this);
+ window.addEventListener("unload", this, false);
+ this._observersAdded = true;
+ }
+
+ let focusElement = () => {
+ this._initialized = true;
+ };
+
+ if (this._onPanelReady) {
+ this._onPanelReady(focusElement);
+ } else {
+ focusElement();
+ }
+ },
+
+ /**
+ * Finds tags that are in common among this._tags entries that track tags
+ * for each selected uri.
+ * The tags arrays should be kept up-to-date for this to work properly.
+ *
+ * @return array of common tags for the selected uris.
+ */
+ _getCommonTags: function() {
+ return this._tags[0].filter(
+ function (aTag) this._tags.every(
+ function (aTags) aTags.indexOf(aTag) != -1
+ ), this
+ );
+ },
+
+ _initTextField: function(aTextFieldId, aValue, aReadOnly) {
+ var field = this._element(aTextFieldId);
+ field.readOnly = aReadOnly !== undefined ? aReadOnly : this._readOnly;
+
+ if (field.value != aValue) {
+ field.value = aValue;
+ this._editorTransactionManagerClear(field);
+ }
+ },
+
+ /**
+ * Appends a menu-item representing a bookmarks folder to a menu-popup.
+ * @param aMenupopup
+ * The popup to which the menu-item should be added.
+ * @param aFolderId
+ * The identifier of the bookmarks folder.
+ * @return the new menu item.
+ */
+ _appendFolderItemToMenupopup:
+ function EIO__appendFolderItemToMenuList(aMenupopup, aFolderId) {
+ // First make sure the folders-separator is visible
+ this._element("foldersSeparator").hidden = false;
+
+ var folderMenuItem = document.createElement("menuitem");
+ var folderTitle = PlacesUtils.bookmarks.getItemTitle(aFolderId)
+ folderMenuItem.folderId = aFolderId;
+ folderMenuItem.setAttribute("label", folderTitle);
+ folderMenuItem.className = "menuitem-iconic folder-icon";
+ aMenupopup.appendChild(folderMenuItem);
+ return folderMenuItem;
+ },
+
+ _initFolderMenuList: function EIO__initFolderMenuList(aSelectedFolder) {
+ // clean up first
+ var menupopup = this._folderMenuList.menupopup;
+ while (menupopup.childNodes.length > 6)
+ menupopup.removeChild(menupopup.lastChild);
+
+ const bms = PlacesUtils.bookmarks;
+ const annos = PlacesUtils.annotations;
+
+ // Build the static list
+ var unfiledItem = this._element("unfiledRootItem");
+ if (!this._staticFoldersListBuilt) {
+ unfiledItem.label = bms.getItemTitle(PlacesUtils.unfiledBookmarksFolderId);
+ unfiledItem.folderId = PlacesUtils.unfiledBookmarksFolderId;
+ var bmMenuItem = this._element("bmRootItem");
+ bmMenuItem.label = bms.getItemTitle(PlacesUtils.bookmarksMenuFolderId);
+ bmMenuItem.folderId = PlacesUtils.bookmarksMenuFolderId;
+ var toolbarItem = this._element("toolbarFolderItem");
+ toolbarItem.label = bms.getItemTitle(PlacesUtils.toolbarFolderId);
+ toolbarItem.folderId = PlacesUtils.toolbarFolderId;
+ this._staticFoldersListBuilt = true;
+ }
+
+ // List of recently used folders:
+ var folderIds = annos.getItemsWithAnnotation(LAST_USED_ANNO);
+
+ /**
+ * The value of the LAST_USED_ANNO annotation is the time (in the form of
+ * Date.getTime) at which the folder has been last used.
+ *
+ * First we build the annotated folders array, each item has both the
+ * folder identifier and the time at which it was last-used by this dialog
+ * set. Then we sort it descendingly based on the time field.
+ */
+ this._recentFolders = [];
+ for (var i = 0; i < folderIds.length; i++) {
+ var lastUsed = annos.getItemAnnotation(folderIds[i], LAST_USED_ANNO);
+ this._recentFolders.push({ folderId: folderIds[i], lastUsed: lastUsed });
+ }
+ this._recentFolders.sort(function(a, b) {
+ if (b.lastUsed < a.lastUsed)
+ return -1;
+ if (b.lastUsed > a.lastUsed)
+ return 1;
+ return 0;
+ });
+
+ var numberOfItems = Math.min(MAX_FOLDER_ITEM_IN_MENU_LIST,
+ this._recentFolders.length);
+ for (var i = 0; i < numberOfItems; i++) {
+ this._appendFolderItemToMenupopup(menupopup,
+ this._recentFolders[i].folderId);
+ }
+
+ var defaultItem = this._getFolderMenuItem(aSelectedFolder);
+ this._folderMenuList.selectedItem = defaultItem;
+
+ // Set a selectedIndex attribute to show special icons
+ this._folderMenuList.setAttribute("selectedIndex",
+ this._folderMenuList.selectedIndex);
+
+ // Hide the folders-separator if no folder is annotated as recently-used
+ this._element("foldersSeparator").hidden = (menupopup.childNodes.length <= 6);
+ this._folderMenuList.disabled = this._readOnly;
+ },
+
+ QueryInterface: function EIO_QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIDOMEventListener) ||
+ aIID.equals(Ci.nsINavBookmarkObserver) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ _element: function EIO__element(aID) {
+ return document.getElementById("editBMPanel_" + aID);
+ },
+
+ _editorTransactionManagerClear: function EIO__editorTransactionManagerClear(aItem) {
+ // Clear the editor's undo stack
+ let transactionManager;
+ try {
+ transactionManager = aItem.editor.transactionManager;
+ } catch (e) {
+ // When retrieving the transaction manager, editor may be null resulting
+ // in a TypeError. Additionally, the transaction manager may not
+ // exist yet, which causes access to it to throw NS_ERROR_FAILURE.
+ // In either event, the transaction manager doesn't exist it, so we
+ // don't need to worry about clearing it.
+ if (!(e instanceof TypeError) && e.result != Cr.NS_ERROR_FAILURE) {
+ throw e;
+ }
+ }
+ if (transactionManager) {
+ transactionManager.clear();
+ }
+ },
+
+ _getItemStaticTitle: function EIO__getItemStaticTitle() {
+ if (this._titleOverride)
+ return this._titleOverride;
+
+ let title = "";
+ if (this._itemId == -1) {
+ title = PlacesUtils.history.getPageTitle(this._uri);
+ }
+ else {
+ title = PlacesUtils.bookmarks.getItemTitle(this._itemId);
+ }
+ return title;
+ },
+
+ _initNamePicker: function EIO_initNamePicker() {
+ var namePicker = this._element("namePicker");
+ namePicker.value = this._getItemStaticTitle();
+ namePicker.readOnly = this._readOnly;
+ this._editorTransactionManagerClear(namePicker);
+ },
+
+ uninitPanel: function EIO_uninitPanel(aHideCollapsibleElements) {
+ if (aHideCollapsibleElements) {
+ // hide the folder tree if it was previously visible
+ var folderTreeRow = this._element("folderTreeRow");
+ if (!folderTreeRow.collapsed)
+ this.toggleFolderTreeVisibility();
+
+ // hide the tag selector if it was previously visible
+ var tagsSelectorRow = this._element("tagsSelectorRow");
+ if (!tagsSelectorRow.collapsed)
+ this.toggleTagsSelector();
+ }
+
+ if (this._observersAdded) {
+ if (this._itemId != -1 || this._uri || this._multiEdit)
+ PlacesUtils.bookmarks.removeObserver(this);
+
+ this._element("namePicker").removeEventListener("blur", this);
+ this._element("locationField").removeEventListener("blur", this);
+ this._element("tagsField").removeEventListener("blur", this);
+ this._element("keywordField").removeEventListener("blur", this);
+ this._element("descriptionField").removeEventListener("blur", this);
+
+ this._observersAdded = false;
+ }
+
+ this._itemId = -1;
+ this._uri = null;
+ this._uris = [];
+ this._tags = [];
+ this._allTags = [];
+ this._itemIds = [];
+ this._multiEdit = false;
+ this._firstEditedField = "";
+ this._initialized = false;
+ this._titleOverride = "";
+ this._readOnly = false;
+ },
+
+ onTagsFieldBlur: function EIO_onTagsFieldBlur() {
+ if (this._updateTags()) // if anything has changed
+ this._mayUpdateFirstEditField("tagsField");
+ },
+
+ _updateTags: function EIO__updateTags() {
+ if (this._multiEdit)
+ return this._updateMultipleTagsForItems();
+ return this._updateSingleTagForItem();
+ },
+
+ _updateSingleTagForItem: function EIO__updateSingleTagForItem() {
+ var currentTags = PlacesUtils.tagging.getTagsForURI(this._uri);
+ var tags = this._getTagsArrayFromTagField();
+ if (tags.length > 0 || currentTags.length > 0) {
+ var tagsToRemove = [];
+ var tagsToAdd = [];
+ var txns = [];
+ for (var i = 0; i < currentTags.length; i++) {
+ if (tags.indexOf(currentTags[i]) == -1)
+ tagsToRemove.push(currentTags[i]);
+ }
+ for (var i = 0; i < tags.length; i++) {
+ if (currentTags.indexOf(tags[i]) == -1)
+ tagsToAdd.push(tags[i]);
+ }
+
+ if (tagsToRemove.length > 0) {
+ let untagTxn = new PlacesUntagURITransaction(this._uri, tagsToRemove);
+ txns.push(untagTxn);
+ }
+ if (tagsToAdd.length > 0) {
+ let tagTxn = new PlacesTagURITransaction(this._uri, tagsToAdd);
+ txns.push(tagTxn);
+ }
+
+ if (txns.length > 0) {
+ let aggregate = new PlacesAggregatedTransaction("Update tags", txns);
+ PlacesUtils.transactionManager.doTransaction(aggregate);
+
+ // Ensure the tagsField is in sync, clean it up from empty tags
+ var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", ");
+ this._initTextField("tagsField", tags, false);
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Stores the first-edit field for this dialog, if the passed-in field
+ * is indeed the first edited field
+ * @param aNewField
+ * the id of the field that may be set (without the "editBMPanel_"
+ * prefix)
+ */
+ _mayUpdateFirstEditField: function EIO__mayUpdateFirstEditField(aNewField) {
+ // * The first-edit-field behavior is not applied in the multi-edit case
+ // * if this._firstEditedField is already set, this is not the first field,
+ // so there's nothing to do
+ if (this._multiEdit || this._firstEditedField)
+ return;
+
+ this._firstEditedField = aNewField;
+
+ // set the pref
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ prefs.setCharPref("browser.bookmarks.editDialog.firstEditField", aNewField);
+ },
+
+ _updateMultipleTagsForItems: function EIO__updateMultipleTagsForItems() {
+ var tags = this._getTagsArrayFromTagField();
+ if (tags.length > 0 || this._allTags.length > 0) {
+ var tagsToRemove = [];
+ var tagsToAdd = [];
+ var txns = [];
+ for (var i = 0; i < this._allTags.length; i++) {
+ if (tags.indexOf(this._allTags[i]) == -1)
+ tagsToRemove.push(this._allTags[i]);
+ }
+ for (var i = 0; i < this._tags.length; i++) {
+ tagsToAdd[i] = [];
+ for (var j = 0; j < tags.length; j++) {
+ if (this._tags[i].indexOf(tags[j]) == -1)
+ tagsToAdd[i].push(tags[j]);
+ }
+ }
+
+ if (tagsToAdd.length > 0) {
+ for (let i = 0; i < this._uris.length; i++) {
+ if (tagsToAdd[i].length > 0) {
+ let tagTxn = new PlacesTagURITransaction(this._uris[i],
+ tagsToAdd[i]);
+ txns.push(tagTxn);
+ }
+ }
+ }
+ if (tagsToRemove.length > 0) {
+ for (let i = 0; i < this._uris.length; i++) {
+ let untagTxn = new PlacesUntagURITransaction(this._uris[i],
+ tagsToRemove);
+ txns.push(untagTxn);
+ }
+ }
+
+ if (txns.length > 0) {
+ let aggregate = new PlacesAggregatedTransaction("Update tags", txns);
+ PlacesUtils.transactionManager.doTransaction(aggregate);
+
+ this._allTags = tags;
+ this._tags = [];
+ for (let i = 0; i < this._uris.length; i++) {
+ this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]);
+ }
+
+ // Ensure the tagsField is in sync, clean it up from empty tags
+ this._initTextField("tagsField", tags, false);
+ return true;
+ }
+ }
+ return false;
+ },
+
+ onNamePickerBlur: function EIO_onNamePickerBlur() {
+ if (this._itemId == -1)
+ return;
+
+ var namePicker = this._element("namePicker")
+
+ // Here we update either the item title or its cached static title
+ var newTitle = namePicker.value;
+ if (!newTitle &&
+ PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) == PlacesUtils.tagsFolderId) {
+ // We don't allow setting an empty title for a tag, restore the old one.
+ this._initNamePicker();
+ }
+ else if (this._getItemStaticTitle() != newTitle) {
+ this._mayUpdateFirstEditField("namePicker");
+ let txn = new PlacesEditItemTitleTransaction(this._itemId, newTitle);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ }
+ },
+
+ onDescriptionFieldBlur: function EIO_onDescriptionFieldBlur() {
+ var description = this._element("descriptionField").value;
+ if (description != PlacesUIUtils.getItemDescription(this._itemId)) {
+ var annoObj = { name : PlacesUIUtils.DESCRIPTION_ANNO,
+ type : Ci.nsIAnnotationService.TYPE_STRING,
+ flags : 0,
+ value : description,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
+ var txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ }
+ },
+
+ onLocationFieldBlur: function EIO_onLocationFieldBlur() {
+ var uri;
+ try {
+ uri = PlacesUIUtils.createFixedURI(this._element("locationField").value);
+ }
+ catch(ex) { return; }
+
+ if (!this._uri.equals(uri)) {
+ var txn = new PlacesEditBookmarkURITransaction(this._itemId, uri);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ this._uri = uri;
+ }
+ },
+
+ onKeywordFieldBlur: function EIO_onKeywordFieldBlur() {
+ let oldKeyword = this._keyword;
+ let keyword = this._keyword = this._element("keywordField").value;
+ if (keyword != oldKeyword) {
+ let txn = new PlacesEditBookmarkKeywordTransaction(this._itemId,
+ keyword,
+ null,
+ oldKeyword);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ }
+ },
+
+ onLoadInSidebarCheckboxCommand:
+ function EIO_onLoadInSidebarCheckboxCommand() {
+ let annoObj = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO };
+ if (this._element("loadInSidebarCheckbox").checked)
+ annoObj.value = true;
+ let txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ },
+
+ toggleFolderTreeVisibility: function EIO_toggleFolderTreeVisibility() {
+ var expander = this._element("foldersExpander");
+ var folderTreeRow = this._element("folderTreeRow");
+ if (!folderTreeRow.collapsed) {
+ expander.className = "expander-down";
+ expander.setAttribute("tooltiptext",
+ expander.getAttribute("tooltiptextdown"));
+ folderTreeRow.collapsed = true;
+ this._element("chooseFolderSeparator").hidden =
+ this._element("chooseFolderMenuItem").hidden = false;
+ }
+ else {
+ expander.className = "expander-up"
+ expander.setAttribute("tooltiptext",
+ expander.getAttribute("tooltiptextup"));
+ folderTreeRow.collapsed = false;
+
+ // XXXmano: Ideally we would only do this once, but for some odd reason,
+ // the editable mode set on this tree, together with its collapsed state
+ // breaks the view.
+ const FOLDER_TREE_PLACE_URI =
+ "place:excludeItems=1&excludeQueries=1&excludeReadOnlyFolders=1&folder=" +
+ PlacesUIUtils.allBookmarksFolderId;
+ this._folderTree.place = FOLDER_TREE_PLACE_URI;
+
+ this._element("chooseFolderSeparator").hidden =
+ this._element("chooseFolderMenuItem").hidden = true;
+ var currentFolder = this._getFolderIdFromMenuList();
+ this._folderTree.selectItems([currentFolder]);
+ this._folderTree.focus();
+ }
+ },
+
+ _getFolderIdFromMenuList:
+ function EIO__getFolderIdFromMenuList() {
+ var selectedItem = this._folderMenuList.selectedItem;
+ NS_ASSERT("folderId" in selectedItem,
+ "Invalid menuitem in the folders-menulist");
+ return selectedItem.folderId;
+ },
+
+ /**
+ * Get the corresponding menu-item in the folder-menu-list for a bookmarks
+ * folder if such an item exists. Otherwise, this creates a menu-item for the
+ * folder. If the items-count limit (see MAX_FOLDERS_IN_MENU_LIST) is reached,
+ * the new item replaces the last menu-item.
+ * @param aFolderId
+ * The identifier of the bookmarks folder.
+ */
+ _getFolderMenuItem:
+ function EIO__getFolderMenuItem(aFolderId) {
+ var menupopup = this._folderMenuList.menupopup;
+
+ for (let i = 0; i < menupopup.childNodes.length; i++) {
+ if ("folderId" in menupopup.childNodes[i] &&
+ menupopup.childNodes[i].folderId == aFolderId)
+ return menupopup.childNodes[i];
+ }
+
+ // 3 special folders + separator + folder-items-count limit
+ if (menupopup.childNodes.length == 4 + MAX_FOLDER_ITEM_IN_MENU_LIST)
+ menupopup.removeChild(menupopup.lastChild);
+
+ return this._appendFolderItemToMenupopup(menupopup, aFolderId);
+ },
+
+ onFolderMenuListCommand: function EIO_onFolderMenuListCommand(aEvent) {
+ // Set a selectedIndex attribute to show special icons
+ this._folderMenuList.setAttribute("selectedIndex",
+ this._folderMenuList.selectedIndex);
+
+ if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") {
+ // reset the selection back to where it was and expand the tree
+ // (this menu-item is hidden when the tree is already visible
+ var container = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
+ var item = this._getFolderMenuItem(container);
+ this._folderMenuList.selectedItem = item;
+ // XXXmano HACK: setTimeout 100, otherwise focus goes back to the
+ // menulist right away
+ setTimeout(function(self) self.toggleFolderTreeVisibility(), 100, this);
+ return;
+ }
+
+ // Move the item
+ var container = this._getFolderIdFromMenuList();
+ if (PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) != container) {
+ var txn = new PlacesMoveItemTransaction(this._itemId,
+ container,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.transactionManager.doTransaction(txn);
+
+ // Mark the containing folder as recently-used if it isn't in the
+ // static list
+ if (container != PlacesUtils.unfiledBookmarksFolderId &&
+ container != PlacesUtils.toolbarFolderId &&
+ container != PlacesUtils.bookmarksMenuFolderId)
+ this._markFolderAsRecentlyUsed(container);
+ }
+
+ // Update folder-tree selection
+ var folderTreeRow = this._element("folderTreeRow");
+ if (!folderTreeRow.collapsed) {
+ var selectedNode = this._folderTree.selectedNode;
+ if (!selectedNode ||
+ PlacesUtils.getConcreteItemId(selectedNode) != container)
+ this._folderTree.selectItems([container]);
+ }
+ },
+
+ onFolderTreeSelect: function EIO_onFolderTreeSelect() {
+ var selectedNode = this._folderTree.selectedNode;
+
+ // Disable the "New Folder" button if we cannot create a new folder
+ this._element("newFolderButton")
+ .disabled = !this._folderTree.insertionPoint || !selectedNode;
+
+ if (!selectedNode)
+ return;
+
+ var folderId = PlacesUtils.getConcreteItemId(selectedNode);
+ if (this._getFolderIdFromMenuList() == folderId)
+ return;
+
+ var folderItem = this._getFolderMenuItem(folderId);
+ this._folderMenuList.selectedItem = folderItem;
+ folderItem.doCommand();
+ },
+
+ _markFolderAsRecentlyUsed:
+ function EIO__markFolderAsRecentlyUsed(aFolderId) {
+ var txns = [];
+
+ // Expire old unused recent folders
+ var anno = this._getLastUsedAnnotationObject(false);
+ while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) {
+ var folderId = this._recentFolders.pop().folderId;
+ let annoTxn = new PlacesSetItemAnnotationTransaction(folderId, anno);
+ txns.push(annoTxn);
+ }
+
+ // Mark folder as recently used
+ anno = this._getLastUsedAnnotationObject(true);
+ let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno);
+ txns.push(annoTxn);
+
+ let aggregate = new PlacesAggregatedTransaction("Update last used folders", txns);
+ PlacesUtils.transactionManager.doTransaction(aggregate);
+ },
+
+ /**
+ * Returns an object which could then be used to set/unset the
+ * LAST_USED_ANNO annotation for a folder.
+ *
+ * @param aLastUsed
+ * Whether to set or unset the LAST_USED_ANNO annotation.
+ * @returns an object representing the annotation which could then be used
+ * with the transaction manager.
+ */
+ _getLastUsedAnnotationObject:
+ function EIO__getLastUsedAnnotationObject(aLastUsed) {
+ var anno = { name: LAST_USED_ANNO,
+ type: Ci.nsIAnnotationService.TYPE_INT32,
+ flags: 0,
+ value: aLastUsed ? new Date().getTime() : null,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
+
+ return anno;
+ },
+
+ _rebuildTagsSelectorList: function EIO__rebuildTagsSelectorList() {
+ var tagsSelector = this._element("tagsSelector");
+ var tagsSelectorRow = this._element("tagsSelectorRow");
+ if (tagsSelectorRow.collapsed)
+ return;
+
+ // Save the current scroll position and restore it after the rebuild.
+ let firstIndex = tagsSelector.getIndexOfFirstVisibleRow();
+ let selectedIndex = tagsSelector.selectedIndex;
+ let selectedTag = selectedIndex >= 0 ? tagsSelector.selectedItem.label
+ : null;
+
+ while (tagsSelector.hasChildNodes())
+ tagsSelector.removeChild(tagsSelector.lastChild);
+
+ var tagsInField = this._getTagsArrayFromTagField();
+ var allTags = PlacesUtils.tagging.allTags;
+ for (var i = 0; i < allTags.length; i++) {
+ var tag = allTags[i];
+ var elt = document.createElement("listitem");
+ elt.setAttribute("type", "checkbox");
+ elt.setAttribute("label", tag);
+ if (tagsInField.indexOf(tag) != -1)
+ elt.setAttribute("checked", "true");
+ tagsSelector.appendChild(elt);
+ if (selectedTag === tag)
+ selectedIndex = tagsSelector.getIndexOfItem(elt);
+ }
+
+ // Restore position.
+ // The listbox allows to scroll only if the required offset doesn't
+ // overflow its capacity, thus need to adjust the index for removals.
+ firstIndex =
+ Math.min(firstIndex,
+ tagsSelector.itemCount - tagsSelector.getNumberOfVisibleRows());
+ tagsSelector.scrollToIndex(firstIndex);
+ if (selectedIndex >= 0 && tagsSelector.itemCount > 0) {
+ selectedIndex = Math.min(selectedIndex, tagsSelector.itemCount - 1);
+ tagsSelector.selectedIndex = selectedIndex;
+ tagsSelector.ensureIndexIsVisible(selectedIndex);
+ }
+ },
+
+ toggleTagsSelector: function EIO_toggleTagsSelector() {
+ var tagsSelector = this._element("tagsSelector");
+ var tagsSelectorRow = this._element("tagsSelectorRow");
+ var expander = this._element("tagsSelectorExpander");
+ if (tagsSelectorRow.collapsed) {
+ expander.className = "expander-up";
+ expander.setAttribute("tooltiptext",
+ expander.getAttribute("tooltiptextup"));
+ tagsSelectorRow.collapsed = false;
+ this._rebuildTagsSelectorList();
+
+ // This is a no-op if we've added the listener.
+ tagsSelector.addEventListener("CheckboxStateChange", this, false);
+ }
+ else {
+ expander.className = "expander-down";
+ expander.setAttribute("tooltiptext",
+ expander.getAttribute("tooltiptextdown"));
+ tagsSelectorRow.collapsed = true;
+ }
+ },
+
+ /**
+ * Splits "tagsField" element value, returning an array of valid tag strings.
+ *
+ * @return Array of tag strings found in the field value.
+ */
+ _getTagsArrayFromTagField: function EIO__getTagsArrayFromTagField() {
+ let tags = this._element("tagsField").value;
+ return tags.trim()
+ .split(/\s*,\s*/) // Split on commas and remove spaces.
+ .filter(function (tag) tag.length > 0); // Kill empty tags.
+ },
+
+ newFolder: function EIO_newFolder() {
+ var ip = this._folderTree.insertionPoint;
+
+ // default to the bookmarks menu folder
+ if (!ip || ip.itemId == PlacesUIUtils.allBookmarksFolderId) {
+ ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ Ci.nsITreeView.DROP_ON);
+ }
+
+ // XXXmano: add a separate "New Folder" string at some point...
+ var defaultLabel = this._element("newFolderButton").label;
+ var txn = new PlacesCreateFolderTransaction(defaultLabel, ip.itemId, ip.index);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ this._folderTree.focus();
+ this._folderTree.selectItems([this._lastNewItem]);
+ this._folderTree.startEditing(this._folderTree.view.selection.currentIndex,
+ this._folderTree.columns.getFirstColumn());
+ },
+
+ // nsIDOMEventListener
+ handleEvent: function EIO_nsIDOMEventListener(aEvent) {
+ switch (aEvent.type) {
+ case "CheckboxStateChange":
+ // Update the tags field when items are checked/unchecked in the listbox
+ var tags = this._getTagsArrayFromTagField();
+
+ if (aEvent.target.checked) {
+ if (tags.indexOf(aEvent.target.label) == -1)
+ tags.push(aEvent.target.label);
+ }
+ else {
+ var indexOfItem = tags.indexOf(aEvent.target.label);
+ if (indexOfItem != -1)
+ tags.splice(indexOfItem, 1);
+ }
+ this._element("tagsField").value = tags.join(", ");
+ this._updateTags();
+ break;
+ case "blur":
+ let replaceFn = (str, firstLetter) => firstLetter.toUpperCase();
+ let nodeName = aEvent.target.id.replace(/editBMPanel_(\w)/, replaceFn);
+ this["on" + nodeName + "Blur"]();
+ break;
+ case "unload":
+ this.uninitPanel(false);
+ break;
+ }
+ },
+
+ // nsINavBookmarkObserver
+ onItemChanged: function EIO_onItemChanged(aItemId, aProperty,
+ aIsAnnotationProperty, aValue,
+ aLastModified, aItemType) {
+ if (aProperty == "tags") {
+ // Tags case is special, since they should be updated if either:
+ // - the notification is for the edited bookmark
+ // - the notification is for the edited history entry
+ // - the notification is for one of edited uris
+ let shouldUpdateTagsField = this._itemId == aItemId;
+ if (this._itemId == -1 || this._multiEdit) {
+ // Check if the changed uri is part of the modified ones.
+ let changedURI = PlacesUtils.bookmarks.getBookmarkURI(aItemId);
+ let uris = this._multiEdit ? this._uris : [this._uri];
+ uris.forEach(function (aURI, aIndex) {
+ if (aURI.equals(changedURI)) {
+ shouldUpdateTagsField = true;
+ if (this._multiEdit) {
+ this._tags[aIndex] = PlacesUtils.tagging.getTagsForURI(this._uris[aIndex]);
+ }
+ }
+ }, this);
+ }
+
+ if (shouldUpdateTagsField) {
+ if (this._multiEdit) {
+ this._allTags = this._getCommonTags();
+ this._initTextField("tagsField", this._allTags.join(", "), false);
+ }
+ else {
+ let tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", ");
+ this._initTextField("tagsField", tags, false);
+ }
+ }
+
+ // Any tags change should be reflected in the tags selector.
+ this._rebuildTagsSelectorList();
+ return;
+ }
+
+ if (this._itemId != aItemId) {
+ if (aProperty == "title") {
+ // If the title of a folder which is listed within the folders
+ // menulist has been changed, we need to update the label of its
+ // representing element.
+ var menupopup = this._folderMenuList.menupopup;
+ for (let i = 0; i < menupopup.childNodes.length; i++) {
+ if ("folderId" in menupopup.childNodes[i] &&
+ menupopup.childNodes[i].folderId == aItemId) {
+ menupopup.childNodes[i].label = aValue;
+ break;
+ }
+ }
+ }
+
+ return;
+ }
+
+ switch (aProperty) {
+ case "title":
+ var namePicker = this._element("namePicker");
+ if (namePicker.value != aValue) {
+ namePicker.value = aValue;
+ this._editorTransactionManagerClear(namePicker);
+ }
+ break;
+ case "uri":
+ var locationField = this._element("locationField");
+ if (locationField.value != aValue) {
+ this._uri = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService).
+ newURI(aValue, null, null);
+ this._initTextField("locationField", this._uri.spec);
+ this._initNamePicker();
+ this._initTextField("tagsField",
+ PlacesUtils.tagging
+ .getTagsForURI(this._uri).join(", "),
+ false);
+ this._rebuildTagsSelectorList();
+ }
+ break;
+ case "keyword":
+ this._keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId);
+ this._initTextField("keywordField", this._keyword);
+ break;
+ case PlacesUIUtils.DESCRIPTION_ANNO:
+ this._initTextField("descriptionField",
+ PlacesUIUtils.getItemDescription(this._itemId));
+ break;
+ case PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO:
+ this._element("loadInSidebarCheckbox").checked =
+ PlacesUtils.annotations.itemHasAnnotation(this._itemId,
+ PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
+ break;
+ case PlacesUtils.LMANNO_FEEDURI:
+ let feedURISpec =
+ PlacesUtils.annotations.getItemAnnotation(this._itemId,
+ PlacesUtils.LMANNO_FEEDURI);
+ this._initTextField("feedLocationField", feedURISpec, true);
+ break;
+ case PlacesUtils.LMANNO_SITEURI:
+ let siteURISpec = "";
+ try {
+ siteURISpec =
+ PlacesUtils.annotations.getItemAnnotation(this._itemId,
+ PlacesUtils.LMANNO_SITEURI);
+ } catch (ex) {}
+ this._initTextField("siteLocationField", siteURISpec, true);
+ break;
+ }
+ },
+
+ onItemMoved: function EIO_onItemMoved(aItemId, aOldParent, aOldIndex,
+ aNewParent, aNewIndex, aItemType) {
+ if (aItemId != this._itemId ||
+ aNewParent == this._getFolderIdFromMenuList())
+ return;
+
+ var folderItem = this._getFolderMenuItem(aNewParent);
+
+ // just setting selectItem _does not_ trigger oncommand, so we don't
+ // recurse
+ this._folderMenuList.selectedItem = folderItem;
+ },
+
+ onItemAdded: function EIO_onItemAdded(aItemId, aParentId, aIndex, aItemType,
+ aURI) {
+ this._lastNewItem = aItemId;
+ },
+
+ onItemRemoved: function() { },
+ onBeginUpdateBatch: function() { },
+ onEndUpdateBatch: function() { },
+ onItemVisited: function() { },
+};