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