browser/components/places/content/editBookmarkOverlay.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed";
     6 const MAX_FOLDER_ITEM_IN_MENU_LIST = 5;
     8 var gEditItemOverlay = {
     9   _uri: null,
    10   _itemId: -1,
    11   _itemIds: [],
    12   _uris: [],
    13   _tags: [],
    14   _allTags: [],
    15   _multiEdit: false,
    16   _itemType: -1,
    17   _readOnly: false,
    18   _hiddenRows: [],
    19   _observersAdded: false,
    20   _staticFoldersListBuilt: false,
    21   _initialized: false,
    22   _titleOverride: "",
    24   // the first field which was edited after this panel was initialized for
    25   // a certain item
    26   _firstEditedField: "",
    28   get itemId() {
    29     return this._itemId;
    30   },
    32   get uri() {
    33     return this._uri;
    34   },
    36   get multiEdit() {
    37     return this._multiEdit;
    38   },
    40   /**
    41    * Determines the initial data for the item edited or added by this dialog
    42    */
    43   _determineInfo: function EIO__determineInfo(aInfo) {
    44     // hidden rows
    45     if (aInfo && aInfo.hiddenRows)
    46       this._hiddenRows = aInfo.hiddenRows;
    47     else
    48       this._hiddenRows.splice(0, this._hiddenRows.length);
    49     // force-read-only
    50     this._readOnly = aInfo && aInfo.forceReadOnly;
    51     this._titleOverride = aInfo && aInfo.titleOverride ? aInfo.titleOverride
    52                                                        : "";
    53   },
    55   _showHideRows: function EIO__showHideRows() {
    56     var isBookmark = this._itemId != -1 &&
    57                      this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK;
    58     var isQuery = false;
    59     if (this._uri)
    60       isQuery = this._uri.schemeIs("place");
    62     this._element("nameRow").collapsed = this._hiddenRows.indexOf("name") != -1;
    63     this._element("folderRow").collapsed =
    64       this._hiddenRows.indexOf("folderPicker") != -1 || this._readOnly;
    65     this._element("tagsRow").collapsed = !this._uri ||
    66       this._hiddenRows.indexOf("tags") != -1 || isQuery;
    67     // Collapse the tag selector if the item does not accept tags.
    68     if (!this._element("tagsSelectorRow").collapsed &&
    69         this._element("tagsRow").collapsed)
    70       this.toggleTagsSelector();
    71     this._element("descriptionRow").collapsed =
    72       this._hiddenRows.indexOf("description") != -1 || this._readOnly;
    73     this._element("keywordRow").collapsed = !isBookmark || this._readOnly ||
    74       this._hiddenRows.indexOf("keyword") != -1 || isQuery;
    75     this._element("locationRow").collapsed = !(this._uri && !isQuery) ||
    76       this._hiddenRows.indexOf("location") != -1;
    77     this._element("loadInSidebarCheckbox").collapsed = !isBookmark || isQuery ||
    78       this._readOnly || this._hiddenRows.indexOf("loadInSidebar") != -1;
    79     this._element("feedLocationRow").collapsed = !this._isLivemark ||
    80       this._hiddenRows.indexOf("feedLocation") != -1;
    81     this._element("siteLocationRow").collapsed = !this._isLivemark ||
    82       this._hiddenRows.indexOf("siteLocation") != -1;
    83     this._element("selectionCount").hidden = !this._multiEdit;
    84   },
    86   /**
    87    * Initialize the panel
    88    * @param aFor
    89    *        Either a places-itemId (of a bookmark, folder or a live bookmark),
    90    *        an array of itemIds (used for bulk tagging), or a URI object (in 
    91    *        which case, the panel would be initialized in read-only mode).
    92    * @param [optional] aInfo
    93    *        JS object which stores additional info for the panel
    94    *        initialization. The following properties may bet set:
    95    *        * hiddenRows (Strings array): list of rows to be hidden regardless
    96    *          of the item edited. Possible values: "title", "location",
    97    *          "description", "keyword", "loadInSidebar", "feedLocation",
    98    *          "siteLocation", folderPicker"
    99    *        * forceReadOnly - set this flag to initialize the panel to its
   100    *          read-only (view) mode even if the given item is editable.
   101    */
   102   initPanel: function EIO_initPanel(aFor, aInfo) {
   103     // For sanity ensure that the implementer has uninited the panel before
   104     // trying to init it again, or we could end up leaking due to observers.
   105     if (this._initialized)
   106       this.uninitPanel(false);
   108     var aItemIdList;
   109     if (Array.isArray(aFor)) {
   110       aItemIdList = aFor;
   111       aFor = aItemIdList[0];
   112     }
   113     else if (this._multiEdit) {
   114       this._multiEdit = false;
   115       this._tags = [];
   116       this._uris = [];
   117       this._allTags = [];
   118       this._itemIds = [];
   119       this._element("selectionCount").hidden = true;
   120     }
   122     this._folderMenuList = this._element("folderMenuList");
   123     this._folderTree = this._element("folderTree");
   125     this._determineInfo(aInfo);
   126     if (aFor instanceof Ci.nsIURI) {
   127       this._itemId = -1;
   128       this._uri = aFor;
   129       this._readOnly = true;
   130     }
   131     else {
   132       this._itemId = aFor;
   133       // We can't store information on invalid itemIds.
   134       this._readOnly = this._readOnly || this._itemId == -1;
   136       var containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
   137       this._itemType = PlacesUtils.bookmarks.getItemType(this._itemId);
   138       if (this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
   139         this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
   140         this._initTextField("keywordField",
   141                             PlacesUtils.bookmarks
   142                                        .getKeywordForBookmark(this._itemId));
   143         this._element("loadInSidebarCheckbox").checked =
   144           PlacesUtils.annotations.itemHasAnnotation(this._itemId,
   145                                                     PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
   146       }
   147       else {
   148         this._uri = null;
   149         this._isLivemark = false;
   150         PlacesUtils.livemarks.getLivemark({id: this._itemId })
   151           .then(aLivemark => {
   152             this._isLivemark = true;
   153             this._initTextField("feedLocationField", aLivemark.feedURI.spec, true);
   154             this._initTextField("siteLocationField", aLivemark.siteURI ? aLivemark.siteURI.spec : "", true);
   155             this._showHideRows();
   156           }, () => undefined);
   157       }
   159       // folder picker
   160       this._initFolderMenuList(containerId);
   162       // description field
   163       this._initTextField("descriptionField", 
   164                           PlacesUIUtils.getItemDescription(this._itemId));
   165     }
   167     if (this._itemId == -1 ||
   168         this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
   169       this._isLivemark = false;
   171       this._initTextField("locationField", this._uri.spec);
   172       if (!aItemIdList) {
   173         var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", ");
   174         this._initTextField("tagsField", tags, false);
   175       }
   176       else {
   177         this._multiEdit = true;
   178         this._allTags = [];
   179         this._itemIds = aItemIdList;
   180         for (var i = 0; i < aItemIdList.length; i++) {
   181           if (aItemIdList[i] instanceof Ci.nsIURI) {
   182             this._uris[i] = aItemIdList[i];
   183             this._itemIds[i] = -1;
   184           }
   185           else
   186             this._uris[i] = PlacesUtils.bookmarks.getBookmarkURI(this._itemIds[i]);
   187           this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]);
   188         }
   189         this._allTags = this._getCommonTags();
   190         this._initTextField("tagsField", this._allTags.join(", "), false);
   191         this._element("itemsCountText").value =
   192           PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
   193                                         this._itemIds.length,
   194                                         [this._itemIds.length]);
   195       }
   197       // tags selector
   198       this._rebuildTagsSelectorList();
   199     }
   201     // name picker
   202     this._initNamePicker();
   204     this._showHideRows();
   206     // observe changes
   207     if (!this._observersAdded) {
   208       // Single bookmarks observe any change.  History entries and multiEdit
   209       // observe only tags changes, through bookmarks.
   210       if (this._itemId != -1 || this._uri || this._multiEdit)
   211         PlacesUtils.bookmarks.addObserver(this, false);
   212       window.addEventListener("unload", this, false);
   213       this._observersAdded = true;
   214     }
   216     this._initialized = true;
   217   },
   219   /**
   220    * Finds tags that are in common among this._tags entries that track tags
   221    * for each selected uri.
   222    * The tags arrays should be kept up-to-date for this to work properly.
   223    *
   224    * @return array of common tags for the selected uris.
   225    */
   226   _getCommonTags: function() {
   227     return this._tags[0].filter(
   228       function (aTag) this._tags.every(
   229         function (aTags) aTags.indexOf(aTag) != -1
   230       ), this
   231     );
   232   },
   234   _initTextField: function(aTextFieldId, aValue, aReadOnly) {
   235     var field = this._element(aTextFieldId);
   236     field.readOnly = aReadOnly !== undefined ? aReadOnly : this._readOnly;
   238     if (field.value != aValue) {
   239       field.value = aValue;
   241       // clear the undo stack
   242       var editor = field.editor;
   243       if (editor)
   244         editor.transactionManager.clear();
   245     }
   246   },
   248   /**
   249    * Appends a menu-item representing a bookmarks folder to a menu-popup.
   250    * @param aMenupopup
   251    *        The popup to which the menu-item should be added.
   252    * @param aFolderId
   253    *        The identifier of the bookmarks folder.
   254    * @return the new menu item.
   255    */
   256   _appendFolderItemToMenupopup:
   257   function EIO__appendFolderItemToMenuList(aMenupopup, aFolderId) {
   258     // First make sure the folders-separator is visible
   259     this._element("foldersSeparator").hidden = false;
   261     var folderMenuItem = document.createElement("menuitem");
   262     var folderTitle = PlacesUtils.bookmarks.getItemTitle(aFolderId)
   263     folderMenuItem.folderId = aFolderId;
   264     folderMenuItem.setAttribute("label", folderTitle);
   265     folderMenuItem.className = "menuitem-iconic folder-icon";
   266     aMenupopup.appendChild(folderMenuItem);
   267     return folderMenuItem;
   268   },
   270   _initFolderMenuList: function EIO__initFolderMenuList(aSelectedFolder) {
   271     // clean up first
   272     var menupopup = this._folderMenuList.menupopup;
   273     while (menupopup.childNodes.length > 6)
   274       menupopup.removeChild(menupopup.lastChild);
   276     const bms = PlacesUtils.bookmarks;
   277     const annos = PlacesUtils.annotations;
   279     // Build the static list
   280     var unfiledItem = this._element("unfiledRootItem");
   281     if (!this._staticFoldersListBuilt) {
   282       unfiledItem.label = bms.getItemTitle(PlacesUtils.unfiledBookmarksFolderId);
   283       unfiledItem.folderId = PlacesUtils.unfiledBookmarksFolderId;
   284       var bmMenuItem = this._element("bmRootItem");
   285       bmMenuItem.label = bms.getItemTitle(PlacesUtils.bookmarksMenuFolderId);
   286       bmMenuItem.folderId = PlacesUtils.bookmarksMenuFolderId;
   287       var toolbarItem = this._element("toolbarFolderItem");
   288       toolbarItem.label = bms.getItemTitle(PlacesUtils.toolbarFolderId);
   289       toolbarItem.folderId = PlacesUtils.toolbarFolderId;
   290       this._staticFoldersListBuilt = true;
   291     }
   293     // List of recently used folders:
   294     var folderIds = annos.getItemsWithAnnotation(LAST_USED_ANNO);
   296     /**
   297      * The value of the LAST_USED_ANNO annotation is the time (in the form of
   298      * Date.getTime) at which the folder has been last used.
   299      *
   300      * First we build the annotated folders array, each item has both the
   301      * folder identifier and the time at which it was last-used by this dialog
   302      * set. Then we sort it descendingly based on the time field.
   303      */
   304     this._recentFolders = [];
   305     for (var i = 0; i < folderIds.length; i++) {
   306       var lastUsed = annos.getItemAnnotation(folderIds[i], LAST_USED_ANNO);
   307       this._recentFolders.push({ folderId: folderIds[i], lastUsed: lastUsed });
   308     }
   309     this._recentFolders.sort(function(a, b) {
   310       if (b.lastUsed < a.lastUsed)
   311         return -1;
   312       if (b.lastUsed > a.lastUsed)
   313         return 1;
   314       return 0;
   315     });
   317     var numberOfItems = Math.min(MAX_FOLDER_ITEM_IN_MENU_LIST,
   318                                  this._recentFolders.length);
   319     for (var i = 0; i < numberOfItems; i++) {
   320       this._appendFolderItemToMenupopup(menupopup,
   321                                         this._recentFolders[i].folderId);
   322     }
   324     var defaultItem = this._getFolderMenuItem(aSelectedFolder);
   325     this._folderMenuList.selectedItem = defaultItem;
   327     // Set a selectedIndex attribute to show special icons
   328     this._folderMenuList.setAttribute("selectedIndex",
   329                                       this._folderMenuList.selectedIndex);
   331     // Hide the folders-separator if no folder is annotated as recently-used
   332     this._element("foldersSeparator").hidden = (menupopup.childNodes.length <= 6);
   333     this._folderMenuList.disabled = this._readOnly;
   334   },
   336   QueryInterface: function EIO_QueryInterface(aIID) {
   337     if (aIID.equals(Ci.nsIDOMEventListener) ||
   338         aIID.equals(Ci.nsINavBookmarkObserver) ||
   339         aIID.equals(Ci.nsISupports))
   340       return this;
   342     throw Cr.NS_ERROR_NO_INTERFACE;
   343   },
   345   _element: function EIO__element(aID) {
   346     return document.getElementById("editBMPanel_" + aID);
   347   },
   349   _getItemStaticTitle: function EIO__getItemStaticTitle() {
   350     if (this._titleOverride)
   351       return this._titleOverride;
   353     let title = "";
   354     if (this._itemId == -1) {
   355       title = PlacesUtils.history.getPageTitle(this._uri);
   356     }
   357     else {
   358       title = PlacesUtils.bookmarks.getItemTitle(this._itemId);
   359     }
   360     return title;
   361   },
   363   _initNamePicker: function EIO_initNamePicker() {
   364     var namePicker = this._element("namePicker");
   365     namePicker.value = this._getItemStaticTitle();
   366     namePicker.readOnly = this._readOnly;
   368     // clear the undo stack
   369     var editor = namePicker.editor;
   370     if (editor)
   371       editor.transactionManager.clear();
   372   },
   374   uninitPanel: function EIO_uninitPanel(aHideCollapsibleElements) {
   375     if (aHideCollapsibleElements) {
   376       // hide the folder tree if it was previously visible
   377       var folderTreeRow = this._element("folderTreeRow");
   378       if (!folderTreeRow.collapsed)
   379         this.toggleFolderTreeVisibility();
   381       // hide the tag selector if it was previously visible
   382       var tagsSelectorRow = this._element("tagsSelectorRow");
   383       if (!tagsSelectorRow.collapsed)
   384         this.toggleTagsSelector();
   385     }
   387     if (this._observersAdded) {
   388       if (this._itemId != -1 || this._uri || this._multiEdit)
   389         PlacesUtils.bookmarks.removeObserver(this);
   391       this._observersAdded = false;
   392     }
   394     this._itemId = -1;
   395     this._uri = null;
   396     this._uris = [];
   397     this._tags = [];
   398     this._allTags = [];
   399     this._itemIds = [];
   400     this._multiEdit = false;
   401     this._firstEditedField = "";
   402     this._initialized = false;
   403     this._titleOverride = "";
   404     this._readOnly = false;
   405   },
   407   onTagsFieldBlur: function EIO_onTagsFieldBlur() {
   408     if (this._updateTags()) // if anything has changed
   409       this._mayUpdateFirstEditField("tagsField");
   410   },
   412   _updateTags: function EIO__updateTags() {
   413     if (this._multiEdit)
   414       return this._updateMultipleTagsForItems();
   415     return this._updateSingleTagForItem();
   416   },
   418   _updateSingleTagForItem: function EIO__updateSingleTagForItem() {
   419     var currentTags = PlacesUtils.tagging.getTagsForURI(this._uri);
   420     var tags = this._getTagsArrayFromTagField();
   421     if (tags.length > 0 || currentTags.length > 0) {
   422       var tagsToRemove = [];
   423       var tagsToAdd = [];
   424       var txns = []; 
   425       for (var i = 0; i < currentTags.length; i++) {
   426         if (tags.indexOf(currentTags[i]) == -1)
   427           tagsToRemove.push(currentTags[i]);
   428       }
   429       for (var i = 0; i < tags.length; i++) {
   430         if (currentTags.indexOf(tags[i]) == -1)
   431           tagsToAdd.push(tags[i]);
   432       }
   434       if (tagsToRemove.length > 0) {
   435         let untagTxn = new PlacesUntagURITransaction(this._uri, tagsToRemove);
   436         txns.push(untagTxn);
   437       }
   438       if (tagsToAdd.length > 0) {
   439         let tagTxn = new PlacesTagURITransaction(this._uri, tagsToAdd);
   440         txns.push(tagTxn);
   441       }
   443       if (txns.length > 0) {
   444         let aggregate = new PlacesAggregatedTransaction("Update tags", txns);
   445         PlacesUtils.transactionManager.doTransaction(aggregate);
   447         // Ensure the tagsField is in sync, clean it up from empty tags
   448         var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", ");
   449         this._initTextField("tagsField", tags, false);
   450         return true;
   451       }
   452     }
   453     return false;
   454   },
   456    /**
   457     * Stores the first-edit field for this dialog, if the passed-in field
   458     * is indeed the first edited field
   459     * @param aNewField
   460     *        the id of the field that may be set (without the "editBMPanel_"
   461     *        prefix)
   462     */
   463   _mayUpdateFirstEditField: function EIO__mayUpdateFirstEditField(aNewField) {
   464     // * The first-edit-field behavior is not applied in the multi-edit case
   465     // * if this._firstEditedField is already set, this is not the first field,
   466     //   so there's nothing to do
   467     if (this._multiEdit || this._firstEditedField)
   468       return;
   470     this._firstEditedField = aNewField;
   472     // set the pref
   473     var prefs = Cc["@mozilla.org/preferences-service;1"].
   474                 getService(Ci.nsIPrefBranch);
   475     prefs.setCharPref("browser.bookmarks.editDialog.firstEditField", aNewField);
   476   },
   478   _updateMultipleTagsForItems: function EIO__updateMultipleTagsForItems() {
   479     var tags = this._getTagsArrayFromTagField();
   480     if (tags.length > 0 || this._allTags.length > 0) {
   481       var tagsToRemove = [];
   482       var tagsToAdd = [];
   483       var txns = []; 
   484       for (var i = 0; i < this._allTags.length; i++) {
   485         if (tags.indexOf(this._allTags[i]) == -1)
   486           tagsToRemove.push(this._allTags[i]);
   487       }
   488       for (var i = 0; i < this._tags.length; i++) {
   489         tagsToAdd[i] = [];
   490         for (var j = 0; j < tags.length; j++) {
   491           if (this._tags[i].indexOf(tags[j]) == -1)
   492             tagsToAdd[i].push(tags[j]);
   493         }
   494       }
   496       if (tagsToAdd.length > 0) {
   497         for (let i = 0; i < this._uris.length; i++) {
   498           if (tagsToAdd[i].length > 0) {
   499             let tagTxn = new PlacesTagURITransaction(this._uris[i],
   500                                                      tagsToAdd[i]);
   501             txns.push(tagTxn);
   502           }
   503         }
   504       }
   505       if (tagsToRemove.length > 0) {
   506         for (let i = 0; i < this._uris.length; i++) {
   507           let untagTxn = new PlacesUntagURITransaction(this._uris[i],
   508                                                        tagsToRemove);
   509           txns.push(untagTxn);
   510         }
   511       }
   513       if (txns.length > 0) {
   514         let aggregate = new PlacesAggregatedTransaction("Update tags", txns);
   515         PlacesUtils.transactionManager.doTransaction(aggregate);
   517         this._allTags = tags;
   518         this._tags = [];
   519         for (let i = 0; i < this._uris.length; i++) {
   520           this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]);
   521         }
   523         // Ensure the tagsField is in sync, clean it up from empty tags
   524         this._initTextField("tagsField", tags, false);
   525         return true;
   526       }
   527     }
   528     return false;
   529   },
   531   onNamePickerChange: function EIO_onNamePickerChange() {
   532     if (this._itemId == -1)
   533       return;
   535     var namePicker = this._element("namePicker")
   537     // Here we update either the item title or its cached static title
   538     var newTitle = namePicker.value;
   539     if (!newTitle &&
   540         PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) == PlacesUtils.tagsFolderId) {
   541       // We don't allow setting an empty title for a tag, restore the old one.
   542       this._initNamePicker();
   543     }
   544     else if (this._getItemStaticTitle() != newTitle) {
   545       this._mayUpdateFirstEditField("namePicker");
   546       let txn = new PlacesEditItemTitleTransaction(this._itemId, newTitle);
   547       PlacesUtils.transactionManager.doTransaction(txn);
   548     }
   549   },
   551   onDescriptionFieldBlur: function EIO_onDescriptionFieldBlur() {
   552     var description = this._element("descriptionField").value;
   553     if (description != PlacesUIUtils.getItemDescription(this._itemId)) {
   554       var annoObj = { name   : PlacesUIUtils.DESCRIPTION_ANNO,
   555                       type   : Ci.nsIAnnotationService.TYPE_STRING,
   556                       flags  : 0,
   557                       value  : description,
   558                       expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
   559       var txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj);
   560       PlacesUtils.transactionManager.doTransaction(txn);
   561     }
   562   },
   564   onLocationFieldBlur: function EIO_onLocationFieldBlur() {
   565     var uri;
   566     try {
   567       uri = PlacesUIUtils.createFixedURI(this._element("locationField").value);
   568     }
   569     catch(ex) { return; }
   571     if (!this._uri.equals(uri)) {
   572       var txn = new PlacesEditBookmarkURITransaction(this._itemId, uri);
   573       PlacesUtils.transactionManager.doTransaction(txn);
   574       this._uri = uri;
   575     }
   576   },
   578   onKeywordFieldBlur: function EIO_onKeywordFieldBlur() {
   579     var keyword = this._element("keywordField").value;
   580     if (keyword != PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId)) {
   581       var txn = new PlacesEditBookmarkKeywordTransaction(this._itemId, keyword);
   582       PlacesUtils.transactionManager.doTransaction(txn);
   583     }
   584   },
   586   onLoadInSidebarCheckboxCommand:
   587   function EIO_onLoadInSidebarCheckboxCommand() {
   588     let annoObj = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO };
   589     if (this._element("loadInSidebarCheckbox").checked)
   590       annoObj.value = true;
   591     let txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj);
   592     PlacesUtils.transactionManager.doTransaction(txn);
   593   },
   595   toggleFolderTreeVisibility: function EIO_toggleFolderTreeVisibility() {
   596     var expander = this._element("foldersExpander");
   597     var folderTreeRow = this._element("folderTreeRow");
   598     if (!folderTreeRow.collapsed) {
   599       expander.className = "expander-down";
   600       expander.setAttribute("tooltiptext",
   601                             expander.getAttribute("tooltiptextdown"));
   602       folderTreeRow.collapsed = true;
   603       this._element("chooseFolderSeparator").hidden =
   604         this._element("chooseFolderMenuItem").hidden = false;
   605     }
   606     else {
   607       expander.className = "expander-up"
   608       expander.setAttribute("tooltiptext",
   609                             expander.getAttribute("tooltiptextup"));
   610       folderTreeRow.collapsed = false;
   612       // XXXmano: Ideally we would only do this once, but for some odd reason,
   613       // the editable mode set on this tree, together with its collapsed state
   614       // breaks the view.
   615       const FOLDER_TREE_PLACE_URI =
   616         "place:excludeItems=1&excludeQueries=1&excludeReadOnlyFolders=1&folder=" +
   617         PlacesUIUtils.allBookmarksFolderId;
   618       this._folderTree.place = FOLDER_TREE_PLACE_URI;
   620       this._element("chooseFolderSeparator").hidden =
   621         this._element("chooseFolderMenuItem").hidden = true;
   622       var currentFolder = this._getFolderIdFromMenuList();
   623       this._folderTree.selectItems([currentFolder]);
   624       this._folderTree.focus();
   625     }
   626   },
   628   _getFolderIdFromMenuList:
   629   function EIO__getFolderIdFromMenuList() {
   630     var selectedItem = this._folderMenuList.selectedItem;
   631     NS_ASSERT("folderId" in selectedItem,
   632               "Invalid menuitem in the folders-menulist");
   633     return selectedItem.folderId;
   634   },
   636   /**
   637    * Get the corresponding menu-item in the folder-menu-list for a bookmarks
   638    * folder if such an item exists. Otherwise, this creates a menu-item for the
   639    * folder. If the items-count limit (see MAX_FOLDERS_IN_MENU_LIST) is reached,
   640    * the new item replaces the last menu-item.
   641    * @param aFolderId
   642    *        The identifier of the bookmarks folder.
   643    */
   644   _getFolderMenuItem:
   645   function EIO__getFolderMenuItem(aFolderId) {
   646     var menupopup = this._folderMenuList.menupopup;
   648     for (let i = 0; i < menupopup.childNodes.length; i++) {
   649       if ("folderId" in menupopup.childNodes[i] &&
   650           menupopup.childNodes[i].folderId == aFolderId)
   651         return menupopup.childNodes[i];
   652     }
   654     // 3 special folders + separator + folder-items-count limit
   655     if (menupopup.childNodes.length == 4 + MAX_FOLDER_ITEM_IN_MENU_LIST)
   656       menupopup.removeChild(menupopup.lastChild);
   658     return this._appendFolderItemToMenupopup(menupopup, aFolderId);
   659   },
   661   onFolderMenuListCommand: function EIO_onFolderMenuListCommand(aEvent) {
   662     // Set a selectedIndex attribute to show special icons
   663     this._folderMenuList.setAttribute("selectedIndex",
   664                                       this._folderMenuList.selectedIndex);
   666     if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") {
   667       // reset the selection back to where it was and expand the tree
   668       // (this menu-item is hidden when the tree is already visible
   669       var container = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
   670       var item = this._getFolderMenuItem(container);
   671       this._folderMenuList.selectedItem = item;
   672       // XXXmano HACK: setTimeout 100, otherwise focus goes back to the
   673       // menulist right away
   674       setTimeout(function(self) self.toggleFolderTreeVisibility(), 100, this);
   675       return;
   676     }
   678     // Move the item
   679     var container = this._getFolderIdFromMenuList();
   680     if (PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) != container) {
   681       var txn = new PlacesMoveItemTransaction(this._itemId, 
   682                                               container, 
   683                                               PlacesUtils.bookmarks.DEFAULT_INDEX);
   684       PlacesUtils.transactionManager.doTransaction(txn);
   686       // Mark the containing folder as recently-used if it isn't in the
   687       // static list
   688       if (container != PlacesUtils.unfiledBookmarksFolderId &&
   689           container != PlacesUtils.toolbarFolderId &&
   690           container != PlacesUtils.bookmarksMenuFolderId)
   691         this._markFolderAsRecentlyUsed(container);
   692     }
   694     // Update folder-tree selection
   695     var folderTreeRow = this._element("folderTreeRow");
   696     if (!folderTreeRow.collapsed) {
   697       var selectedNode = this._folderTree.selectedNode;
   698       if (!selectedNode ||
   699           PlacesUtils.getConcreteItemId(selectedNode) != container)
   700         this._folderTree.selectItems([container]);
   701     }
   702   },
   704   onFolderTreeSelect: function EIO_onFolderTreeSelect() {
   705     var selectedNode = this._folderTree.selectedNode;
   707     // Disable the "New Folder" button if we cannot create a new folder
   708     this._element("newFolderButton")
   709         .disabled = !this._folderTree.insertionPoint || !selectedNode;
   711     if (!selectedNode)
   712       return;
   714     var folderId = PlacesUtils.getConcreteItemId(selectedNode);
   715     if (this._getFolderIdFromMenuList() == folderId)
   716       return;
   718     var folderItem = this._getFolderMenuItem(folderId);
   719     this._folderMenuList.selectedItem = folderItem;
   720     folderItem.doCommand();
   721   },
   723   _markFolderAsRecentlyUsed:
   724   function EIO__markFolderAsRecentlyUsed(aFolderId) {
   725     var txns = [];
   727     // Expire old unused recent folders
   728     var anno = this._getLastUsedAnnotationObject(false);
   729     while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) {
   730       var folderId = this._recentFolders.pop().folderId;
   731       let annoTxn = new PlacesSetItemAnnotationTransaction(folderId, anno);
   732       txns.push(annoTxn);
   733     }
   735     // Mark folder as recently used
   736     anno = this._getLastUsedAnnotationObject(true);
   737     let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno);
   738     txns.push(annoTxn);
   740     let aggregate = new PlacesAggregatedTransaction("Update last used folders", txns);
   741     PlacesUtils.transactionManager.doTransaction(aggregate);
   742   },
   744   /**
   745    * Returns an object which could then be used to set/unset the
   746    * LAST_USED_ANNO annotation for a folder.
   747    *
   748    * @param aLastUsed
   749    *        Whether to set or unset the LAST_USED_ANNO annotation.
   750    * @returns an object representing the annotation which could then be used
   751    *          with the transaction manager.
   752    */
   753   _getLastUsedAnnotationObject:
   754   function EIO__getLastUsedAnnotationObject(aLastUsed) {
   755     var anno = { name: LAST_USED_ANNO,
   756                  type: Ci.nsIAnnotationService.TYPE_INT32,
   757                  flags: 0,
   758                  value: aLastUsed ? new Date().getTime() : null,
   759                  expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
   761     return anno;
   762   },
   764   _rebuildTagsSelectorList: function EIO__rebuildTagsSelectorList() {
   765     var tagsSelector = this._element("tagsSelector");
   766     var tagsSelectorRow = this._element("tagsSelectorRow");
   767     if (tagsSelectorRow.collapsed)
   768       return;
   770     // Save the current scroll position and restore it after the rebuild.
   771     let firstIndex = tagsSelector.getIndexOfFirstVisibleRow();
   772     let selectedIndex = tagsSelector.selectedIndex;
   773     let selectedTag = selectedIndex >= 0 ? tagsSelector.selectedItem.label
   774                                          : null;
   776     while (tagsSelector.hasChildNodes())
   777       tagsSelector.removeChild(tagsSelector.lastChild);
   779     var tagsInField = this._getTagsArrayFromTagField();
   780     var allTags = PlacesUtils.tagging.allTags;
   781     for (var i = 0; i < allTags.length; i++) {
   782       var tag = allTags[i];
   783       var elt = document.createElement("listitem");
   784       elt.setAttribute("type", "checkbox");
   785       elt.setAttribute("label", tag);
   786       if (tagsInField.indexOf(tag) != -1)
   787         elt.setAttribute("checked", "true");
   788       tagsSelector.appendChild(elt);
   789       if (selectedTag === tag)
   790         selectedIndex = tagsSelector.getIndexOfItem(elt);
   791     }
   793     // Restore position.
   794     // The listbox allows to scroll only if the required offset doesn't
   795     // overflow its capacity, thus need to adjust the index for removals.
   796     firstIndex =
   797       Math.min(firstIndex,
   798                tagsSelector.itemCount - tagsSelector.getNumberOfVisibleRows());
   799     tagsSelector.scrollToIndex(firstIndex);
   800     if (selectedIndex >= 0 && tagsSelector.itemCount > 0) {
   801       selectedIndex = Math.min(selectedIndex, tagsSelector.itemCount - 1);
   802       tagsSelector.selectedIndex = selectedIndex;
   803       tagsSelector.ensureIndexIsVisible(selectedIndex);
   804     }
   805   },
   807   toggleTagsSelector: function EIO_toggleTagsSelector() {
   808     var tagsSelector = this._element("tagsSelector");
   809     var tagsSelectorRow = this._element("tagsSelectorRow");
   810     var expander = this._element("tagsSelectorExpander");
   811     if (tagsSelectorRow.collapsed) {
   812       expander.className = "expander-up";
   813       expander.setAttribute("tooltiptext",
   814                             expander.getAttribute("tooltiptextup"));
   815       tagsSelectorRow.collapsed = false;
   816       this._rebuildTagsSelectorList();
   818       // This is a no-op if we've added the listener.
   819       tagsSelector.addEventListener("CheckboxStateChange", this, false);
   820     }
   821     else {
   822       expander.className = "expander-down";
   823       expander.setAttribute("tooltiptext",
   824                             expander.getAttribute("tooltiptextdown"));
   825       tagsSelectorRow.collapsed = true;
   826     }
   827   },
   829   /**
   830    * Splits "tagsField" element value, returning an array of valid tag strings.
   831    *
   832    * @return Array of tag strings found in the field value.
   833    */
   834   _getTagsArrayFromTagField: function EIO__getTagsArrayFromTagField() {
   835     let tags = this._element("tagsField").value;
   836     return tags.trim()
   837                .split(/\s*,\s*/) // Split on commas and remove spaces.
   838                .filter(function (tag) tag.length > 0); // Kill empty tags.
   839   },
   841   newFolder: function EIO_newFolder() {
   842     var ip = this._folderTree.insertionPoint;
   844     // default to the bookmarks menu folder
   845     if (!ip || ip.itemId == PlacesUIUtils.allBookmarksFolderId) {
   846         ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
   847                                 PlacesUtils.bookmarks.DEFAULT_INDEX,
   848                                 Ci.nsITreeView.DROP_ON);
   849     }
   851     // XXXmano: add a separate "New Folder" string at some point...
   852     var defaultLabel = this._element("newFolderButton").label;
   853     var txn = new PlacesCreateFolderTransaction(defaultLabel, ip.itemId, ip.index);
   854     PlacesUtils.transactionManager.doTransaction(txn);
   855     this._folderTree.focus();
   856     this._folderTree.selectItems([ip.itemId]);
   857     PlacesUtils.asContainer(this._folderTree.selectedNode).containerOpen = true;
   858     this._folderTree.selectItems([this._lastNewItem]);
   859     this._folderTree.startEditing(this._folderTree.view.selection.currentIndex,
   860                                   this._folderTree.columns.getFirstColumn());
   861   },
   863   // nsIDOMEventListener
   864   handleEvent: function EIO_nsIDOMEventListener(aEvent) {
   865     switch (aEvent.type) {
   866     case "CheckboxStateChange":
   867       // Update the tags field when items are checked/unchecked in the listbox
   868       var tags = this._getTagsArrayFromTagField();
   870       if (aEvent.target.checked) {
   871         if (tags.indexOf(aEvent.target.label) == -1)
   872           tags.push(aEvent.target.label);
   873       }
   874       else {
   875         var indexOfItem = tags.indexOf(aEvent.target.label);
   876         if (indexOfItem != -1)
   877           tags.splice(indexOfItem, 1);
   878       }
   879       this._element("tagsField").value = tags.join(", ");
   880       this._updateTags();
   881       break;
   882     case "unload":
   883       this.uninitPanel(false);
   884       break;
   885     }
   886   },
   888   // nsINavBookmarkObserver
   889   onItemChanged: function EIO_onItemChanged(aItemId, aProperty,
   890                                             aIsAnnotationProperty, aValue,
   891                                             aLastModified, aItemType) {
   892     if (aProperty == "tags") {
   893       // Tags case is special, since they should be updated if either:
   894       // - the notification is for the edited bookmark
   895       // - the notification is for the edited history entry
   896       // - the notification is for one of edited uris
   897       let shouldUpdateTagsField = this._itemId == aItemId;
   898       if (this._itemId == -1 || this._multiEdit) {
   899         // Check if the changed uri is part of the modified ones.
   900         let changedURI = PlacesUtils.bookmarks.getBookmarkURI(aItemId);
   901         let uris = this._multiEdit ? this._uris : [this._uri];
   902         uris.forEach(function (aURI, aIndex) {
   903           if (aURI.equals(changedURI)) {
   904             shouldUpdateTagsField = true;
   905             if (this._multiEdit) {
   906               this._tags[aIndex] = PlacesUtils.tagging.getTagsForURI(this._uris[aIndex]);
   907             }
   908           }
   909         }, this);
   910       }
   912       if (shouldUpdateTagsField) {
   913         if (this._multiEdit) {
   914           this._allTags = this._getCommonTags();
   915           this._initTextField("tagsField", this._allTags.join(", "), false);
   916         }
   917         else {
   918           let tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", ");
   919           this._initTextField("tagsField", tags, false);
   920         }
   921       }
   923       // Any tags change should be reflected in the tags selector.
   924       this._rebuildTagsSelectorList();
   925       return;
   926     }
   928     if (this._itemId != aItemId) {
   929       if (aProperty == "title") {
   930         // If the title of a folder which is listed within the folders
   931         // menulist has been changed, we need to update the label of its
   932         // representing element.
   933         var menupopup = this._folderMenuList.menupopup;
   934         for (let i = 0; i < menupopup.childNodes.length; i++) {
   935           if ("folderId" in menupopup.childNodes[i] &&
   936               menupopup.childNodes[i].folderId == aItemId) {
   937             menupopup.childNodes[i].label = aValue;
   938             break;
   939           }
   940         }
   941       }
   943       return;
   944     }
   946     switch (aProperty) {
   947     case "title":
   948       var namePicker = this._element("namePicker");
   949       if (namePicker.value != aValue) {
   950         namePicker.value = aValue;
   951         // clear undo stack
   952         namePicker.editor.transactionManager.clear();
   953       }
   954       break;
   955     case "uri":
   956       var locationField = this._element("locationField");
   957       if (locationField.value != aValue) {
   958         this._uri = Cc["@mozilla.org/network/io-service;1"].
   959                     getService(Ci.nsIIOService).
   960                     newURI(aValue, null, null);
   961         this._initTextField("locationField", this._uri.spec);
   962         this._initNamePicker();
   963         this._initTextField("tagsField",
   964                              PlacesUtils.tagging
   965                                         .getTagsForURI(this._uri).join(", "),
   966                             false);
   967         this._rebuildTagsSelectorList();
   968       }
   969       break;
   970     case "keyword":
   971       this._initTextField("keywordField",
   972                           PlacesUtils.bookmarks
   973                                      .getKeywordForBookmark(this._itemId));
   974       break;
   975     case PlacesUIUtils.DESCRIPTION_ANNO:
   976       this._initTextField("descriptionField",
   977                           PlacesUIUtils.getItemDescription(this._itemId));
   978       break;
   979     case PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO:
   980       this._element("loadInSidebarCheckbox").checked =
   981         PlacesUtils.annotations.itemHasAnnotation(this._itemId,
   982                                                   PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
   983       break;
   984     case PlacesUtils.LMANNO_FEEDURI:
   985       let feedURISpec =
   986         PlacesUtils.annotations.getItemAnnotation(this._itemId,
   987                                                   PlacesUtils.LMANNO_FEEDURI);
   988       this._initTextField("feedLocationField", feedURISpec, true);
   989       break;
   990     case PlacesUtils.LMANNO_SITEURI:
   991       let siteURISpec = "";
   992       try {
   993         siteURISpec =
   994           PlacesUtils.annotations.getItemAnnotation(this._itemId,
   995                                                     PlacesUtils.LMANNO_SITEURI);
   996       } catch (ex) {}
   997       this._initTextField("siteLocationField", siteURISpec, true);
   998       break;
   999     }
  1000   },
  1002   onItemMoved: function EIO_onItemMoved(aItemId, aOldParent, aOldIndex,
  1003                                         aNewParent, aNewIndex, aItemType) {
  1004     if (aItemId != this._itemId ||
  1005         aNewParent == this._getFolderIdFromMenuList())
  1006       return;
  1008     var folderItem = this._getFolderMenuItem(aNewParent);
  1010     // just setting selectItem _does not_ trigger oncommand, so we don't
  1011     // recurse
  1012     this._folderMenuList.selectedItem = folderItem;
  1013   },
  1015   onItemAdded: function EIO_onItemAdded(aItemId, aParentId, aIndex, aItemType,
  1016                                         aURI) {
  1017     this._lastNewItem = aItemId;
  1018   },
  1020   onItemRemoved: function() { },
  1021   onBeginUpdateBatch: function() { },
  1022   onEndUpdateBatch: function() { },
  1023   onItemVisited: function() { },
  1024 };

mercurial