/* 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() { }, };