1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/places/content/editBookmarkOverlay.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1024 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed"; 1.9 +const MAX_FOLDER_ITEM_IN_MENU_LIST = 5; 1.10 + 1.11 +var gEditItemOverlay = { 1.12 + _uri: null, 1.13 + _itemId: -1, 1.14 + _itemIds: [], 1.15 + _uris: [], 1.16 + _tags: [], 1.17 + _allTags: [], 1.18 + _multiEdit: false, 1.19 + _itemType: -1, 1.20 + _readOnly: false, 1.21 + _hiddenRows: [], 1.22 + _observersAdded: false, 1.23 + _staticFoldersListBuilt: false, 1.24 + _initialized: false, 1.25 + _titleOverride: "", 1.26 + 1.27 + // the first field which was edited after this panel was initialized for 1.28 + // a certain item 1.29 + _firstEditedField: "", 1.30 + 1.31 + get itemId() { 1.32 + return this._itemId; 1.33 + }, 1.34 + 1.35 + get uri() { 1.36 + return this._uri; 1.37 + }, 1.38 + 1.39 + get multiEdit() { 1.40 + return this._multiEdit; 1.41 + }, 1.42 + 1.43 + /** 1.44 + * Determines the initial data for the item edited or added by this dialog 1.45 + */ 1.46 + _determineInfo: function EIO__determineInfo(aInfo) { 1.47 + // hidden rows 1.48 + if (aInfo && aInfo.hiddenRows) 1.49 + this._hiddenRows = aInfo.hiddenRows; 1.50 + else 1.51 + this._hiddenRows.splice(0, this._hiddenRows.length); 1.52 + // force-read-only 1.53 + this._readOnly = aInfo && aInfo.forceReadOnly; 1.54 + this._titleOverride = aInfo && aInfo.titleOverride ? aInfo.titleOverride 1.55 + : ""; 1.56 + }, 1.57 + 1.58 + _showHideRows: function EIO__showHideRows() { 1.59 + var isBookmark = this._itemId != -1 && 1.60 + this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK; 1.61 + var isQuery = false; 1.62 + if (this._uri) 1.63 + isQuery = this._uri.schemeIs("place"); 1.64 + 1.65 + this._element("nameRow").collapsed = this._hiddenRows.indexOf("name") != -1; 1.66 + this._element("folderRow").collapsed = 1.67 + this._hiddenRows.indexOf("folderPicker") != -1 || this._readOnly; 1.68 + this._element("tagsRow").collapsed = !this._uri || 1.69 + this._hiddenRows.indexOf("tags") != -1 || isQuery; 1.70 + // Collapse the tag selector if the item does not accept tags. 1.71 + if (!this._element("tagsSelectorRow").collapsed && 1.72 + this._element("tagsRow").collapsed) 1.73 + this.toggleTagsSelector(); 1.74 + this._element("descriptionRow").collapsed = 1.75 + this._hiddenRows.indexOf("description") != -1 || this._readOnly; 1.76 + this._element("keywordRow").collapsed = !isBookmark || this._readOnly || 1.77 + this._hiddenRows.indexOf("keyword") != -1 || isQuery; 1.78 + this._element("locationRow").collapsed = !(this._uri && !isQuery) || 1.79 + this._hiddenRows.indexOf("location") != -1; 1.80 + this._element("loadInSidebarCheckbox").collapsed = !isBookmark || isQuery || 1.81 + this._readOnly || this._hiddenRows.indexOf("loadInSidebar") != -1; 1.82 + this._element("feedLocationRow").collapsed = !this._isLivemark || 1.83 + this._hiddenRows.indexOf("feedLocation") != -1; 1.84 + this._element("siteLocationRow").collapsed = !this._isLivemark || 1.85 + this._hiddenRows.indexOf("siteLocation") != -1; 1.86 + this._element("selectionCount").hidden = !this._multiEdit; 1.87 + }, 1.88 + 1.89 + /** 1.90 + * Initialize the panel 1.91 + * @param aFor 1.92 + * Either a places-itemId (of a bookmark, folder or a live bookmark), 1.93 + * an array of itemIds (used for bulk tagging), or a URI object (in 1.94 + * which case, the panel would be initialized in read-only mode). 1.95 + * @param [optional] aInfo 1.96 + * JS object which stores additional info for the panel 1.97 + * initialization. The following properties may bet set: 1.98 + * * hiddenRows (Strings array): list of rows to be hidden regardless 1.99 + * of the item edited. Possible values: "title", "location", 1.100 + * "description", "keyword", "loadInSidebar", "feedLocation", 1.101 + * "siteLocation", folderPicker" 1.102 + * * forceReadOnly - set this flag to initialize the panel to its 1.103 + * read-only (view) mode even if the given item is editable. 1.104 + */ 1.105 + initPanel: function EIO_initPanel(aFor, aInfo) { 1.106 + // For sanity ensure that the implementer has uninited the panel before 1.107 + // trying to init it again, or we could end up leaking due to observers. 1.108 + if (this._initialized) 1.109 + this.uninitPanel(false); 1.110 + 1.111 + var aItemIdList; 1.112 + if (Array.isArray(aFor)) { 1.113 + aItemIdList = aFor; 1.114 + aFor = aItemIdList[0]; 1.115 + } 1.116 + else if (this._multiEdit) { 1.117 + this._multiEdit = false; 1.118 + this._tags = []; 1.119 + this._uris = []; 1.120 + this._allTags = []; 1.121 + this._itemIds = []; 1.122 + this._element("selectionCount").hidden = true; 1.123 + } 1.124 + 1.125 + this._folderMenuList = this._element("folderMenuList"); 1.126 + this._folderTree = this._element("folderTree"); 1.127 + 1.128 + this._determineInfo(aInfo); 1.129 + if (aFor instanceof Ci.nsIURI) { 1.130 + this._itemId = -1; 1.131 + this._uri = aFor; 1.132 + this._readOnly = true; 1.133 + } 1.134 + else { 1.135 + this._itemId = aFor; 1.136 + // We can't store information on invalid itemIds. 1.137 + this._readOnly = this._readOnly || this._itemId == -1; 1.138 + 1.139 + var containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId); 1.140 + this._itemType = PlacesUtils.bookmarks.getItemType(this._itemId); 1.141 + if (this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) { 1.142 + this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId); 1.143 + this._initTextField("keywordField", 1.144 + PlacesUtils.bookmarks 1.145 + .getKeywordForBookmark(this._itemId)); 1.146 + this._element("loadInSidebarCheckbox").checked = 1.147 + PlacesUtils.annotations.itemHasAnnotation(this._itemId, 1.148 + PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); 1.149 + } 1.150 + else { 1.151 + this._uri = null; 1.152 + this._isLivemark = false; 1.153 + PlacesUtils.livemarks.getLivemark({id: this._itemId }) 1.154 + .then(aLivemark => { 1.155 + this._isLivemark = true; 1.156 + this._initTextField("feedLocationField", aLivemark.feedURI.spec, true); 1.157 + this._initTextField("siteLocationField", aLivemark.siteURI ? aLivemark.siteURI.spec : "", true); 1.158 + this._showHideRows(); 1.159 + }, () => undefined); 1.160 + } 1.161 + 1.162 + // folder picker 1.163 + this._initFolderMenuList(containerId); 1.164 + 1.165 + // description field 1.166 + this._initTextField("descriptionField", 1.167 + PlacesUIUtils.getItemDescription(this._itemId)); 1.168 + } 1.169 + 1.170 + if (this._itemId == -1 || 1.171 + this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) { 1.172 + this._isLivemark = false; 1.173 + 1.174 + this._initTextField("locationField", this._uri.spec); 1.175 + if (!aItemIdList) { 1.176 + var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); 1.177 + this._initTextField("tagsField", tags, false); 1.178 + } 1.179 + else { 1.180 + this._multiEdit = true; 1.181 + this._allTags = []; 1.182 + this._itemIds = aItemIdList; 1.183 + for (var i = 0; i < aItemIdList.length; i++) { 1.184 + if (aItemIdList[i] instanceof Ci.nsIURI) { 1.185 + this._uris[i] = aItemIdList[i]; 1.186 + this._itemIds[i] = -1; 1.187 + } 1.188 + else 1.189 + this._uris[i] = PlacesUtils.bookmarks.getBookmarkURI(this._itemIds[i]); 1.190 + this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]); 1.191 + } 1.192 + this._allTags = this._getCommonTags(); 1.193 + this._initTextField("tagsField", this._allTags.join(", "), false); 1.194 + this._element("itemsCountText").value = 1.195 + PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", 1.196 + this._itemIds.length, 1.197 + [this._itemIds.length]); 1.198 + } 1.199 + 1.200 + // tags selector 1.201 + this._rebuildTagsSelectorList(); 1.202 + } 1.203 + 1.204 + // name picker 1.205 + this._initNamePicker(); 1.206 + 1.207 + this._showHideRows(); 1.208 + 1.209 + // observe changes 1.210 + if (!this._observersAdded) { 1.211 + // Single bookmarks observe any change. History entries and multiEdit 1.212 + // observe only tags changes, through bookmarks. 1.213 + if (this._itemId != -1 || this._uri || this._multiEdit) 1.214 + PlacesUtils.bookmarks.addObserver(this, false); 1.215 + window.addEventListener("unload", this, false); 1.216 + this._observersAdded = true; 1.217 + } 1.218 + 1.219 + this._initialized = true; 1.220 + }, 1.221 + 1.222 + /** 1.223 + * Finds tags that are in common among this._tags entries that track tags 1.224 + * for each selected uri. 1.225 + * The tags arrays should be kept up-to-date for this to work properly. 1.226 + * 1.227 + * @return array of common tags for the selected uris. 1.228 + */ 1.229 + _getCommonTags: function() { 1.230 + return this._tags[0].filter( 1.231 + function (aTag) this._tags.every( 1.232 + function (aTags) aTags.indexOf(aTag) != -1 1.233 + ), this 1.234 + ); 1.235 + }, 1.236 + 1.237 + _initTextField: function(aTextFieldId, aValue, aReadOnly) { 1.238 + var field = this._element(aTextFieldId); 1.239 + field.readOnly = aReadOnly !== undefined ? aReadOnly : this._readOnly; 1.240 + 1.241 + if (field.value != aValue) { 1.242 + field.value = aValue; 1.243 + 1.244 + // clear the undo stack 1.245 + var editor = field.editor; 1.246 + if (editor) 1.247 + editor.transactionManager.clear(); 1.248 + } 1.249 + }, 1.250 + 1.251 + /** 1.252 + * Appends a menu-item representing a bookmarks folder to a menu-popup. 1.253 + * @param aMenupopup 1.254 + * The popup to which the menu-item should be added. 1.255 + * @param aFolderId 1.256 + * The identifier of the bookmarks folder. 1.257 + * @return the new menu item. 1.258 + */ 1.259 + _appendFolderItemToMenupopup: 1.260 + function EIO__appendFolderItemToMenuList(aMenupopup, aFolderId) { 1.261 + // First make sure the folders-separator is visible 1.262 + this._element("foldersSeparator").hidden = false; 1.263 + 1.264 + var folderMenuItem = document.createElement("menuitem"); 1.265 + var folderTitle = PlacesUtils.bookmarks.getItemTitle(aFolderId) 1.266 + folderMenuItem.folderId = aFolderId; 1.267 + folderMenuItem.setAttribute("label", folderTitle); 1.268 + folderMenuItem.className = "menuitem-iconic folder-icon"; 1.269 + aMenupopup.appendChild(folderMenuItem); 1.270 + return folderMenuItem; 1.271 + }, 1.272 + 1.273 + _initFolderMenuList: function EIO__initFolderMenuList(aSelectedFolder) { 1.274 + // clean up first 1.275 + var menupopup = this._folderMenuList.menupopup; 1.276 + while (menupopup.childNodes.length > 6) 1.277 + menupopup.removeChild(menupopup.lastChild); 1.278 + 1.279 + const bms = PlacesUtils.bookmarks; 1.280 + const annos = PlacesUtils.annotations; 1.281 + 1.282 + // Build the static list 1.283 + var unfiledItem = this._element("unfiledRootItem"); 1.284 + if (!this._staticFoldersListBuilt) { 1.285 + unfiledItem.label = bms.getItemTitle(PlacesUtils.unfiledBookmarksFolderId); 1.286 + unfiledItem.folderId = PlacesUtils.unfiledBookmarksFolderId; 1.287 + var bmMenuItem = this._element("bmRootItem"); 1.288 + bmMenuItem.label = bms.getItemTitle(PlacesUtils.bookmarksMenuFolderId); 1.289 + bmMenuItem.folderId = PlacesUtils.bookmarksMenuFolderId; 1.290 + var toolbarItem = this._element("toolbarFolderItem"); 1.291 + toolbarItem.label = bms.getItemTitle(PlacesUtils.toolbarFolderId); 1.292 + toolbarItem.folderId = PlacesUtils.toolbarFolderId; 1.293 + this._staticFoldersListBuilt = true; 1.294 + } 1.295 + 1.296 + // List of recently used folders: 1.297 + var folderIds = annos.getItemsWithAnnotation(LAST_USED_ANNO); 1.298 + 1.299 + /** 1.300 + * The value of the LAST_USED_ANNO annotation is the time (in the form of 1.301 + * Date.getTime) at which the folder has been last used. 1.302 + * 1.303 + * First we build the annotated folders array, each item has both the 1.304 + * folder identifier and the time at which it was last-used by this dialog 1.305 + * set. Then we sort it descendingly based on the time field. 1.306 + */ 1.307 + this._recentFolders = []; 1.308 + for (var i = 0; i < folderIds.length; i++) { 1.309 + var lastUsed = annos.getItemAnnotation(folderIds[i], LAST_USED_ANNO); 1.310 + this._recentFolders.push({ folderId: folderIds[i], lastUsed: lastUsed }); 1.311 + } 1.312 + this._recentFolders.sort(function(a, b) { 1.313 + if (b.lastUsed < a.lastUsed) 1.314 + return -1; 1.315 + if (b.lastUsed > a.lastUsed) 1.316 + return 1; 1.317 + return 0; 1.318 + }); 1.319 + 1.320 + var numberOfItems = Math.min(MAX_FOLDER_ITEM_IN_MENU_LIST, 1.321 + this._recentFolders.length); 1.322 + for (var i = 0; i < numberOfItems; i++) { 1.323 + this._appendFolderItemToMenupopup(menupopup, 1.324 + this._recentFolders[i].folderId); 1.325 + } 1.326 + 1.327 + var defaultItem = this._getFolderMenuItem(aSelectedFolder); 1.328 + this._folderMenuList.selectedItem = defaultItem; 1.329 + 1.330 + // Set a selectedIndex attribute to show special icons 1.331 + this._folderMenuList.setAttribute("selectedIndex", 1.332 + this._folderMenuList.selectedIndex); 1.333 + 1.334 + // Hide the folders-separator if no folder is annotated as recently-used 1.335 + this._element("foldersSeparator").hidden = (menupopup.childNodes.length <= 6); 1.336 + this._folderMenuList.disabled = this._readOnly; 1.337 + }, 1.338 + 1.339 + QueryInterface: function EIO_QueryInterface(aIID) { 1.340 + if (aIID.equals(Ci.nsIDOMEventListener) || 1.341 + aIID.equals(Ci.nsINavBookmarkObserver) || 1.342 + aIID.equals(Ci.nsISupports)) 1.343 + return this; 1.344 + 1.345 + throw Cr.NS_ERROR_NO_INTERFACE; 1.346 + }, 1.347 + 1.348 + _element: function EIO__element(aID) { 1.349 + return document.getElementById("editBMPanel_" + aID); 1.350 + }, 1.351 + 1.352 + _getItemStaticTitle: function EIO__getItemStaticTitle() { 1.353 + if (this._titleOverride) 1.354 + return this._titleOverride; 1.355 + 1.356 + let title = ""; 1.357 + if (this._itemId == -1) { 1.358 + title = PlacesUtils.history.getPageTitle(this._uri); 1.359 + } 1.360 + else { 1.361 + title = PlacesUtils.bookmarks.getItemTitle(this._itemId); 1.362 + } 1.363 + return title; 1.364 + }, 1.365 + 1.366 + _initNamePicker: function EIO_initNamePicker() { 1.367 + var namePicker = this._element("namePicker"); 1.368 + namePicker.value = this._getItemStaticTitle(); 1.369 + namePicker.readOnly = this._readOnly; 1.370 + 1.371 + // clear the undo stack 1.372 + var editor = namePicker.editor; 1.373 + if (editor) 1.374 + editor.transactionManager.clear(); 1.375 + }, 1.376 + 1.377 + uninitPanel: function EIO_uninitPanel(aHideCollapsibleElements) { 1.378 + if (aHideCollapsibleElements) { 1.379 + // hide the folder tree if it was previously visible 1.380 + var folderTreeRow = this._element("folderTreeRow"); 1.381 + if (!folderTreeRow.collapsed) 1.382 + this.toggleFolderTreeVisibility(); 1.383 + 1.384 + // hide the tag selector if it was previously visible 1.385 + var tagsSelectorRow = this._element("tagsSelectorRow"); 1.386 + if (!tagsSelectorRow.collapsed) 1.387 + this.toggleTagsSelector(); 1.388 + } 1.389 + 1.390 + if (this._observersAdded) { 1.391 + if (this._itemId != -1 || this._uri || this._multiEdit) 1.392 + PlacesUtils.bookmarks.removeObserver(this); 1.393 + 1.394 + this._observersAdded = false; 1.395 + } 1.396 + 1.397 + this._itemId = -1; 1.398 + this._uri = null; 1.399 + this._uris = []; 1.400 + this._tags = []; 1.401 + this._allTags = []; 1.402 + this._itemIds = []; 1.403 + this._multiEdit = false; 1.404 + this._firstEditedField = ""; 1.405 + this._initialized = false; 1.406 + this._titleOverride = ""; 1.407 + this._readOnly = false; 1.408 + }, 1.409 + 1.410 + onTagsFieldBlur: function EIO_onTagsFieldBlur() { 1.411 + if (this._updateTags()) // if anything has changed 1.412 + this._mayUpdateFirstEditField("tagsField"); 1.413 + }, 1.414 + 1.415 + _updateTags: function EIO__updateTags() { 1.416 + if (this._multiEdit) 1.417 + return this._updateMultipleTagsForItems(); 1.418 + return this._updateSingleTagForItem(); 1.419 + }, 1.420 + 1.421 + _updateSingleTagForItem: function EIO__updateSingleTagForItem() { 1.422 + var currentTags = PlacesUtils.tagging.getTagsForURI(this._uri); 1.423 + var tags = this._getTagsArrayFromTagField(); 1.424 + if (tags.length > 0 || currentTags.length > 0) { 1.425 + var tagsToRemove = []; 1.426 + var tagsToAdd = []; 1.427 + var txns = []; 1.428 + for (var i = 0; i < currentTags.length; i++) { 1.429 + if (tags.indexOf(currentTags[i]) == -1) 1.430 + tagsToRemove.push(currentTags[i]); 1.431 + } 1.432 + for (var i = 0; i < tags.length; i++) { 1.433 + if (currentTags.indexOf(tags[i]) == -1) 1.434 + tagsToAdd.push(tags[i]); 1.435 + } 1.436 + 1.437 + if (tagsToRemove.length > 0) { 1.438 + let untagTxn = new PlacesUntagURITransaction(this._uri, tagsToRemove); 1.439 + txns.push(untagTxn); 1.440 + } 1.441 + if (tagsToAdd.length > 0) { 1.442 + let tagTxn = new PlacesTagURITransaction(this._uri, tagsToAdd); 1.443 + txns.push(tagTxn); 1.444 + } 1.445 + 1.446 + if (txns.length > 0) { 1.447 + let aggregate = new PlacesAggregatedTransaction("Update tags", txns); 1.448 + PlacesUtils.transactionManager.doTransaction(aggregate); 1.449 + 1.450 + // Ensure the tagsField is in sync, clean it up from empty tags 1.451 + var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); 1.452 + this._initTextField("tagsField", tags, false); 1.453 + return true; 1.454 + } 1.455 + } 1.456 + return false; 1.457 + }, 1.458 + 1.459 + /** 1.460 + * Stores the first-edit field for this dialog, if the passed-in field 1.461 + * is indeed the first edited field 1.462 + * @param aNewField 1.463 + * the id of the field that may be set (without the "editBMPanel_" 1.464 + * prefix) 1.465 + */ 1.466 + _mayUpdateFirstEditField: function EIO__mayUpdateFirstEditField(aNewField) { 1.467 + // * The first-edit-field behavior is not applied in the multi-edit case 1.468 + // * if this._firstEditedField is already set, this is not the first field, 1.469 + // so there's nothing to do 1.470 + if (this._multiEdit || this._firstEditedField) 1.471 + return; 1.472 + 1.473 + this._firstEditedField = aNewField; 1.474 + 1.475 + // set the pref 1.476 + var prefs = Cc["@mozilla.org/preferences-service;1"]. 1.477 + getService(Ci.nsIPrefBranch); 1.478 + prefs.setCharPref("browser.bookmarks.editDialog.firstEditField", aNewField); 1.479 + }, 1.480 + 1.481 + _updateMultipleTagsForItems: function EIO__updateMultipleTagsForItems() { 1.482 + var tags = this._getTagsArrayFromTagField(); 1.483 + if (tags.length > 0 || this._allTags.length > 0) { 1.484 + var tagsToRemove = []; 1.485 + var tagsToAdd = []; 1.486 + var txns = []; 1.487 + for (var i = 0; i < this._allTags.length; i++) { 1.488 + if (tags.indexOf(this._allTags[i]) == -1) 1.489 + tagsToRemove.push(this._allTags[i]); 1.490 + } 1.491 + for (var i = 0; i < this._tags.length; i++) { 1.492 + tagsToAdd[i] = []; 1.493 + for (var j = 0; j < tags.length; j++) { 1.494 + if (this._tags[i].indexOf(tags[j]) == -1) 1.495 + tagsToAdd[i].push(tags[j]); 1.496 + } 1.497 + } 1.498 + 1.499 + if (tagsToAdd.length > 0) { 1.500 + for (let i = 0; i < this._uris.length; i++) { 1.501 + if (tagsToAdd[i].length > 0) { 1.502 + let tagTxn = new PlacesTagURITransaction(this._uris[i], 1.503 + tagsToAdd[i]); 1.504 + txns.push(tagTxn); 1.505 + } 1.506 + } 1.507 + } 1.508 + if (tagsToRemove.length > 0) { 1.509 + for (let i = 0; i < this._uris.length; i++) { 1.510 + let untagTxn = new PlacesUntagURITransaction(this._uris[i], 1.511 + tagsToRemove); 1.512 + txns.push(untagTxn); 1.513 + } 1.514 + } 1.515 + 1.516 + if (txns.length > 0) { 1.517 + let aggregate = new PlacesAggregatedTransaction("Update tags", txns); 1.518 + PlacesUtils.transactionManager.doTransaction(aggregate); 1.519 + 1.520 + this._allTags = tags; 1.521 + this._tags = []; 1.522 + for (let i = 0; i < this._uris.length; i++) { 1.523 + this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]); 1.524 + } 1.525 + 1.526 + // Ensure the tagsField is in sync, clean it up from empty tags 1.527 + this._initTextField("tagsField", tags, false); 1.528 + return true; 1.529 + } 1.530 + } 1.531 + return false; 1.532 + }, 1.533 + 1.534 + onNamePickerChange: function EIO_onNamePickerChange() { 1.535 + if (this._itemId == -1) 1.536 + return; 1.537 + 1.538 + var namePicker = this._element("namePicker") 1.539 + 1.540 + // Here we update either the item title or its cached static title 1.541 + var newTitle = namePicker.value; 1.542 + if (!newTitle && 1.543 + PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) == PlacesUtils.tagsFolderId) { 1.544 + // We don't allow setting an empty title for a tag, restore the old one. 1.545 + this._initNamePicker(); 1.546 + } 1.547 + else if (this._getItemStaticTitle() != newTitle) { 1.548 + this._mayUpdateFirstEditField("namePicker"); 1.549 + let txn = new PlacesEditItemTitleTransaction(this._itemId, newTitle); 1.550 + PlacesUtils.transactionManager.doTransaction(txn); 1.551 + } 1.552 + }, 1.553 + 1.554 + onDescriptionFieldBlur: function EIO_onDescriptionFieldBlur() { 1.555 + var description = this._element("descriptionField").value; 1.556 + if (description != PlacesUIUtils.getItemDescription(this._itemId)) { 1.557 + var annoObj = { name : PlacesUIUtils.DESCRIPTION_ANNO, 1.558 + type : Ci.nsIAnnotationService.TYPE_STRING, 1.559 + flags : 0, 1.560 + value : description, 1.561 + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; 1.562 + var txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj); 1.563 + PlacesUtils.transactionManager.doTransaction(txn); 1.564 + } 1.565 + }, 1.566 + 1.567 + onLocationFieldBlur: function EIO_onLocationFieldBlur() { 1.568 + var uri; 1.569 + try { 1.570 + uri = PlacesUIUtils.createFixedURI(this._element("locationField").value); 1.571 + } 1.572 + catch(ex) { return; } 1.573 + 1.574 + if (!this._uri.equals(uri)) { 1.575 + var txn = new PlacesEditBookmarkURITransaction(this._itemId, uri); 1.576 + PlacesUtils.transactionManager.doTransaction(txn); 1.577 + this._uri = uri; 1.578 + } 1.579 + }, 1.580 + 1.581 + onKeywordFieldBlur: function EIO_onKeywordFieldBlur() { 1.582 + var keyword = this._element("keywordField").value; 1.583 + if (keyword != PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId)) { 1.584 + var txn = new PlacesEditBookmarkKeywordTransaction(this._itemId, keyword); 1.585 + PlacesUtils.transactionManager.doTransaction(txn); 1.586 + } 1.587 + }, 1.588 + 1.589 + onLoadInSidebarCheckboxCommand: 1.590 + function EIO_onLoadInSidebarCheckboxCommand() { 1.591 + let annoObj = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO }; 1.592 + if (this._element("loadInSidebarCheckbox").checked) 1.593 + annoObj.value = true; 1.594 + let txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj); 1.595 + PlacesUtils.transactionManager.doTransaction(txn); 1.596 + }, 1.597 + 1.598 + toggleFolderTreeVisibility: function EIO_toggleFolderTreeVisibility() { 1.599 + var expander = this._element("foldersExpander"); 1.600 + var folderTreeRow = this._element("folderTreeRow"); 1.601 + if (!folderTreeRow.collapsed) { 1.602 + expander.className = "expander-down"; 1.603 + expander.setAttribute("tooltiptext", 1.604 + expander.getAttribute("tooltiptextdown")); 1.605 + folderTreeRow.collapsed = true; 1.606 + this._element("chooseFolderSeparator").hidden = 1.607 + this._element("chooseFolderMenuItem").hidden = false; 1.608 + } 1.609 + else { 1.610 + expander.className = "expander-up" 1.611 + expander.setAttribute("tooltiptext", 1.612 + expander.getAttribute("tooltiptextup")); 1.613 + folderTreeRow.collapsed = false; 1.614 + 1.615 + // XXXmano: Ideally we would only do this once, but for some odd reason, 1.616 + // the editable mode set on this tree, together with its collapsed state 1.617 + // breaks the view. 1.618 + const FOLDER_TREE_PLACE_URI = 1.619 + "place:excludeItems=1&excludeQueries=1&excludeReadOnlyFolders=1&folder=" + 1.620 + PlacesUIUtils.allBookmarksFolderId; 1.621 + this._folderTree.place = FOLDER_TREE_PLACE_URI; 1.622 + 1.623 + this._element("chooseFolderSeparator").hidden = 1.624 + this._element("chooseFolderMenuItem").hidden = true; 1.625 + var currentFolder = this._getFolderIdFromMenuList(); 1.626 + this._folderTree.selectItems([currentFolder]); 1.627 + this._folderTree.focus(); 1.628 + } 1.629 + }, 1.630 + 1.631 + _getFolderIdFromMenuList: 1.632 + function EIO__getFolderIdFromMenuList() { 1.633 + var selectedItem = this._folderMenuList.selectedItem; 1.634 + NS_ASSERT("folderId" in selectedItem, 1.635 + "Invalid menuitem in the folders-menulist"); 1.636 + return selectedItem.folderId; 1.637 + }, 1.638 + 1.639 + /** 1.640 + * Get the corresponding menu-item in the folder-menu-list for a bookmarks 1.641 + * folder if such an item exists. Otherwise, this creates a menu-item for the 1.642 + * folder. If the items-count limit (see MAX_FOLDERS_IN_MENU_LIST) is reached, 1.643 + * the new item replaces the last menu-item. 1.644 + * @param aFolderId 1.645 + * The identifier of the bookmarks folder. 1.646 + */ 1.647 + _getFolderMenuItem: 1.648 + function EIO__getFolderMenuItem(aFolderId) { 1.649 + var menupopup = this._folderMenuList.menupopup; 1.650 + 1.651 + for (let i = 0; i < menupopup.childNodes.length; i++) { 1.652 + if ("folderId" in menupopup.childNodes[i] && 1.653 + menupopup.childNodes[i].folderId == aFolderId) 1.654 + return menupopup.childNodes[i]; 1.655 + } 1.656 + 1.657 + // 3 special folders + separator + folder-items-count limit 1.658 + if (menupopup.childNodes.length == 4 + MAX_FOLDER_ITEM_IN_MENU_LIST) 1.659 + menupopup.removeChild(menupopup.lastChild); 1.660 + 1.661 + return this._appendFolderItemToMenupopup(menupopup, aFolderId); 1.662 + }, 1.663 + 1.664 + onFolderMenuListCommand: function EIO_onFolderMenuListCommand(aEvent) { 1.665 + // Set a selectedIndex attribute to show special icons 1.666 + this._folderMenuList.setAttribute("selectedIndex", 1.667 + this._folderMenuList.selectedIndex); 1.668 + 1.669 + if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") { 1.670 + // reset the selection back to where it was and expand the tree 1.671 + // (this menu-item is hidden when the tree is already visible 1.672 + var container = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId); 1.673 + var item = this._getFolderMenuItem(container); 1.674 + this._folderMenuList.selectedItem = item; 1.675 + // XXXmano HACK: setTimeout 100, otherwise focus goes back to the 1.676 + // menulist right away 1.677 + setTimeout(function(self) self.toggleFolderTreeVisibility(), 100, this); 1.678 + return; 1.679 + } 1.680 + 1.681 + // Move the item 1.682 + var container = this._getFolderIdFromMenuList(); 1.683 + if (PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) != container) { 1.684 + var txn = new PlacesMoveItemTransaction(this._itemId, 1.685 + container, 1.686 + PlacesUtils.bookmarks.DEFAULT_INDEX); 1.687 + PlacesUtils.transactionManager.doTransaction(txn); 1.688 + 1.689 + // Mark the containing folder as recently-used if it isn't in the 1.690 + // static list 1.691 + if (container != PlacesUtils.unfiledBookmarksFolderId && 1.692 + container != PlacesUtils.toolbarFolderId && 1.693 + container != PlacesUtils.bookmarksMenuFolderId) 1.694 + this._markFolderAsRecentlyUsed(container); 1.695 + } 1.696 + 1.697 + // Update folder-tree selection 1.698 + var folderTreeRow = this._element("folderTreeRow"); 1.699 + if (!folderTreeRow.collapsed) { 1.700 + var selectedNode = this._folderTree.selectedNode; 1.701 + if (!selectedNode || 1.702 + PlacesUtils.getConcreteItemId(selectedNode) != container) 1.703 + this._folderTree.selectItems([container]); 1.704 + } 1.705 + }, 1.706 + 1.707 + onFolderTreeSelect: function EIO_onFolderTreeSelect() { 1.708 + var selectedNode = this._folderTree.selectedNode; 1.709 + 1.710 + // Disable the "New Folder" button if we cannot create a new folder 1.711 + this._element("newFolderButton") 1.712 + .disabled = !this._folderTree.insertionPoint || !selectedNode; 1.713 + 1.714 + if (!selectedNode) 1.715 + return; 1.716 + 1.717 + var folderId = PlacesUtils.getConcreteItemId(selectedNode); 1.718 + if (this._getFolderIdFromMenuList() == folderId) 1.719 + return; 1.720 + 1.721 + var folderItem = this._getFolderMenuItem(folderId); 1.722 + this._folderMenuList.selectedItem = folderItem; 1.723 + folderItem.doCommand(); 1.724 + }, 1.725 + 1.726 + _markFolderAsRecentlyUsed: 1.727 + function EIO__markFolderAsRecentlyUsed(aFolderId) { 1.728 + var txns = []; 1.729 + 1.730 + // Expire old unused recent folders 1.731 + var anno = this._getLastUsedAnnotationObject(false); 1.732 + while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) { 1.733 + var folderId = this._recentFolders.pop().folderId; 1.734 + let annoTxn = new PlacesSetItemAnnotationTransaction(folderId, anno); 1.735 + txns.push(annoTxn); 1.736 + } 1.737 + 1.738 + // Mark folder as recently used 1.739 + anno = this._getLastUsedAnnotationObject(true); 1.740 + let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno); 1.741 + txns.push(annoTxn); 1.742 + 1.743 + let aggregate = new PlacesAggregatedTransaction("Update last used folders", txns); 1.744 + PlacesUtils.transactionManager.doTransaction(aggregate); 1.745 + }, 1.746 + 1.747 + /** 1.748 + * Returns an object which could then be used to set/unset the 1.749 + * LAST_USED_ANNO annotation for a folder. 1.750 + * 1.751 + * @param aLastUsed 1.752 + * Whether to set or unset the LAST_USED_ANNO annotation. 1.753 + * @returns an object representing the annotation which could then be used 1.754 + * with the transaction manager. 1.755 + */ 1.756 + _getLastUsedAnnotationObject: 1.757 + function EIO__getLastUsedAnnotationObject(aLastUsed) { 1.758 + var anno = { name: LAST_USED_ANNO, 1.759 + type: Ci.nsIAnnotationService.TYPE_INT32, 1.760 + flags: 0, 1.761 + value: aLastUsed ? new Date().getTime() : null, 1.762 + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; 1.763 + 1.764 + return anno; 1.765 + }, 1.766 + 1.767 + _rebuildTagsSelectorList: function EIO__rebuildTagsSelectorList() { 1.768 + var tagsSelector = this._element("tagsSelector"); 1.769 + var tagsSelectorRow = this._element("tagsSelectorRow"); 1.770 + if (tagsSelectorRow.collapsed) 1.771 + return; 1.772 + 1.773 + // Save the current scroll position and restore it after the rebuild. 1.774 + let firstIndex = tagsSelector.getIndexOfFirstVisibleRow(); 1.775 + let selectedIndex = tagsSelector.selectedIndex; 1.776 + let selectedTag = selectedIndex >= 0 ? tagsSelector.selectedItem.label 1.777 + : null; 1.778 + 1.779 + while (tagsSelector.hasChildNodes()) 1.780 + tagsSelector.removeChild(tagsSelector.lastChild); 1.781 + 1.782 + var tagsInField = this._getTagsArrayFromTagField(); 1.783 + var allTags = PlacesUtils.tagging.allTags; 1.784 + for (var i = 0; i < allTags.length; i++) { 1.785 + var tag = allTags[i]; 1.786 + var elt = document.createElement("listitem"); 1.787 + elt.setAttribute("type", "checkbox"); 1.788 + elt.setAttribute("label", tag); 1.789 + if (tagsInField.indexOf(tag) != -1) 1.790 + elt.setAttribute("checked", "true"); 1.791 + tagsSelector.appendChild(elt); 1.792 + if (selectedTag === tag) 1.793 + selectedIndex = tagsSelector.getIndexOfItem(elt); 1.794 + } 1.795 + 1.796 + // Restore position. 1.797 + // The listbox allows to scroll only if the required offset doesn't 1.798 + // overflow its capacity, thus need to adjust the index for removals. 1.799 + firstIndex = 1.800 + Math.min(firstIndex, 1.801 + tagsSelector.itemCount - tagsSelector.getNumberOfVisibleRows()); 1.802 + tagsSelector.scrollToIndex(firstIndex); 1.803 + if (selectedIndex >= 0 && tagsSelector.itemCount > 0) { 1.804 + selectedIndex = Math.min(selectedIndex, tagsSelector.itemCount - 1); 1.805 + tagsSelector.selectedIndex = selectedIndex; 1.806 + tagsSelector.ensureIndexIsVisible(selectedIndex); 1.807 + } 1.808 + }, 1.809 + 1.810 + toggleTagsSelector: function EIO_toggleTagsSelector() { 1.811 + var tagsSelector = this._element("tagsSelector"); 1.812 + var tagsSelectorRow = this._element("tagsSelectorRow"); 1.813 + var expander = this._element("tagsSelectorExpander"); 1.814 + if (tagsSelectorRow.collapsed) { 1.815 + expander.className = "expander-up"; 1.816 + expander.setAttribute("tooltiptext", 1.817 + expander.getAttribute("tooltiptextup")); 1.818 + tagsSelectorRow.collapsed = false; 1.819 + this._rebuildTagsSelectorList(); 1.820 + 1.821 + // This is a no-op if we've added the listener. 1.822 + tagsSelector.addEventListener("CheckboxStateChange", this, false); 1.823 + } 1.824 + else { 1.825 + expander.className = "expander-down"; 1.826 + expander.setAttribute("tooltiptext", 1.827 + expander.getAttribute("tooltiptextdown")); 1.828 + tagsSelectorRow.collapsed = true; 1.829 + } 1.830 + }, 1.831 + 1.832 + /** 1.833 + * Splits "tagsField" element value, returning an array of valid tag strings. 1.834 + * 1.835 + * @return Array of tag strings found in the field value. 1.836 + */ 1.837 + _getTagsArrayFromTagField: function EIO__getTagsArrayFromTagField() { 1.838 + let tags = this._element("tagsField").value; 1.839 + return tags.trim() 1.840 + .split(/\s*,\s*/) // Split on commas and remove spaces. 1.841 + .filter(function (tag) tag.length > 0); // Kill empty tags. 1.842 + }, 1.843 + 1.844 + newFolder: function EIO_newFolder() { 1.845 + var ip = this._folderTree.insertionPoint; 1.846 + 1.847 + // default to the bookmarks menu folder 1.848 + if (!ip || ip.itemId == PlacesUIUtils.allBookmarksFolderId) { 1.849 + ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, 1.850 + PlacesUtils.bookmarks.DEFAULT_INDEX, 1.851 + Ci.nsITreeView.DROP_ON); 1.852 + } 1.853 + 1.854 + // XXXmano: add a separate "New Folder" string at some point... 1.855 + var defaultLabel = this._element("newFolderButton").label; 1.856 + var txn = new PlacesCreateFolderTransaction(defaultLabel, ip.itemId, ip.index); 1.857 + PlacesUtils.transactionManager.doTransaction(txn); 1.858 + this._folderTree.focus(); 1.859 + this._folderTree.selectItems([ip.itemId]); 1.860 + PlacesUtils.asContainer(this._folderTree.selectedNode).containerOpen = true; 1.861 + this._folderTree.selectItems([this._lastNewItem]); 1.862 + this._folderTree.startEditing(this._folderTree.view.selection.currentIndex, 1.863 + this._folderTree.columns.getFirstColumn()); 1.864 + }, 1.865 + 1.866 + // nsIDOMEventListener 1.867 + handleEvent: function EIO_nsIDOMEventListener(aEvent) { 1.868 + switch (aEvent.type) { 1.869 + case "CheckboxStateChange": 1.870 + // Update the tags field when items are checked/unchecked in the listbox 1.871 + var tags = this._getTagsArrayFromTagField(); 1.872 + 1.873 + if (aEvent.target.checked) { 1.874 + if (tags.indexOf(aEvent.target.label) == -1) 1.875 + tags.push(aEvent.target.label); 1.876 + } 1.877 + else { 1.878 + var indexOfItem = tags.indexOf(aEvent.target.label); 1.879 + if (indexOfItem != -1) 1.880 + tags.splice(indexOfItem, 1); 1.881 + } 1.882 + this._element("tagsField").value = tags.join(", "); 1.883 + this._updateTags(); 1.884 + break; 1.885 + case "unload": 1.886 + this.uninitPanel(false); 1.887 + break; 1.888 + } 1.889 + }, 1.890 + 1.891 + // nsINavBookmarkObserver 1.892 + onItemChanged: function EIO_onItemChanged(aItemId, aProperty, 1.893 + aIsAnnotationProperty, aValue, 1.894 + aLastModified, aItemType) { 1.895 + if (aProperty == "tags") { 1.896 + // Tags case is special, since they should be updated if either: 1.897 + // - the notification is for the edited bookmark 1.898 + // - the notification is for the edited history entry 1.899 + // - the notification is for one of edited uris 1.900 + let shouldUpdateTagsField = this._itemId == aItemId; 1.901 + if (this._itemId == -1 || this._multiEdit) { 1.902 + // Check if the changed uri is part of the modified ones. 1.903 + let changedURI = PlacesUtils.bookmarks.getBookmarkURI(aItemId); 1.904 + let uris = this._multiEdit ? this._uris : [this._uri]; 1.905 + uris.forEach(function (aURI, aIndex) { 1.906 + if (aURI.equals(changedURI)) { 1.907 + shouldUpdateTagsField = true; 1.908 + if (this._multiEdit) { 1.909 + this._tags[aIndex] = PlacesUtils.tagging.getTagsForURI(this._uris[aIndex]); 1.910 + } 1.911 + } 1.912 + }, this); 1.913 + } 1.914 + 1.915 + if (shouldUpdateTagsField) { 1.916 + if (this._multiEdit) { 1.917 + this._allTags = this._getCommonTags(); 1.918 + this._initTextField("tagsField", this._allTags.join(", "), false); 1.919 + } 1.920 + else { 1.921 + let tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); 1.922 + this._initTextField("tagsField", tags, false); 1.923 + } 1.924 + } 1.925 + 1.926 + // Any tags change should be reflected in the tags selector. 1.927 + this._rebuildTagsSelectorList(); 1.928 + return; 1.929 + } 1.930 + 1.931 + if (this._itemId != aItemId) { 1.932 + if (aProperty == "title") { 1.933 + // If the title of a folder which is listed within the folders 1.934 + // menulist has been changed, we need to update the label of its 1.935 + // representing element. 1.936 + var menupopup = this._folderMenuList.menupopup; 1.937 + for (let i = 0; i < menupopup.childNodes.length; i++) { 1.938 + if ("folderId" in menupopup.childNodes[i] && 1.939 + menupopup.childNodes[i].folderId == aItemId) { 1.940 + menupopup.childNodes[i].label = aValue; 1.941 + break; 1.942 + } 1.943 + } 1.944 + } 1.945 + 1.946 + return; 1.947 + } 1.948 + 1.949 + switch (aProperty) { 1.950 + case "title": 1.951 + var namePicker = this._element("namePicker"); 1.952 + if (namePicker.value != aValue) { 1.953 + namePicker.value = aValue; 1.954 + // clear undo stack 1.955 + namePicker.editor.transactionManager.clear(); 1.956 + } 1.957 + break; 1.958 + case "uri": 1.959 + var locationField = this._element("locationField"); 1.960 + if (locationField.value != aValue) { 1.961 + this._uri = Cc["@mozilla.org/network/io-service;1"]. 1.962 + getService(Ci.nsIIOService). 1.963 + newURI(aValue, null, null); 1.964 + this._initTextField("locationField", this._uri.spec); 1.965 + this._initNamePicker(); 1.966 + this._initTextField("tagsField", 1.967 + PlacesUtils.tagging 1.968 + .getTagsForURI(this._uri).join(", "), 1.969 + false); 1.970 + this._rebuildTagsSelectorList(); 1.971 + } 1.972 + break; 1.973 + case "keyword": 1.974 + this._initTextField("keywordField", 1.975 + PlacesUtils.bookmarks 1.976 + .getKeywordForBookmark(this._itemId)); 1.977 + break; 1.978 + case PlacesUIUtils.DESCRIPTION_ANNO: 1.979 + this._initTextField("descriptionField", 1.980 + PlacesUIUtils.getItemDescription(this._itemId)); 1.981 + break; 1.982 + case PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO: 1.983 + this._element("loadInSidebarCheckbox").checked = 1.984 + PlacesUtils.annotations.itemHasAnnotation(this._itemId, 1.985 + PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); 1.986 + break; 1.987 + case PlacesUtils.LMANNO_FEEDURI: 1.988 + let feedURISpec = 1.989 + PlacesUtils.annotations.getItemAnnotation(this._itemId, 1.990 + PlacesUtils.LMANNO_FEEDURI); 1.991 + this._initTextField("feedLocationField", feedURISpec, true); 1.992 + break; 1.993 + case PlacesUtils.LMANNO_SITEURI: 1.994 + let siteURISpec = ""; 1.995 + try { 1.996 + siteURISpec = 1.997 + PlacesUtils.annotations.getItemAnnotation(this._itemId, 1.998 + PlacesUtils.LMANNO_SITEURI); 1.999 + } catch (ex) {} 1.1000 + this._initTextField("siteLocationField", siteURISpec, true); 1.1001 + break; 1.1002 + } 1.1003 + }, 1.1004 + 1.1005 + onItemMoved: function EIO_onItemMoved(aItemId, aOldParent, aOldIndex, 1.1006 + aNewParent, aNewIndex, aItemType) { 1.1007 + if (aItemId != this._itemId || 1.1008 + aNewParent == this._getFolderIdFromMenuList()) 1.1009 + return; 1.1010 + 1.1011 + var folderItem = this._getFolderMenuItem(aNewParent); 1.1012 + 1.1013 + // just setting selectItem _does not_ trigger oncommand, so we don't 1.1014 + // recurse 1.1015 + this._folderMenuList.selectedItem = folderItem; 1.1016 + }, 1.1017 + 1.1018 + onItemAdded: function EIO_onItemAdded(aItemId, aParentId, aIndex, aItemType, 1.1019 + aURI) { 1.1020 + this._lastNewItem = aItemId; 1.1021 + }, 1.1022 + 1.1023 + onItemRemoved: function() { }, 1.1024 + onBeginUpdateBatch: function() { }, 1.1025 + onEndUpdateBatch: function() { }, 1.1026 + onItemVisited: function() { }, 1.1027 +};