home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / lib / firefox-3.0.14 / chrome / browser.jar / content / browser / places / editBookmarkOverlay.js < prev    next >
Encoding:
Text File  |  2008-04-21  |  34.6 KB  |  944 lines

  1. /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  2. /* ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License Version
  6.  * 1.1 (the "License"); you may not use this file except in compliance with
  7.  * the License. You may obtain a copy of the License at
  8.  * http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS" basis,
  11.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12.  * for the specific language governing rights and limitations under the
  13.  * License.
  14.  *
  15.  * The Original Code is the Places Bookmark Properties dialog.
  16.  *
  17.  * The Initial Developer of the Original Code is Google Inc.
  18.  * Portions created by the Initial Developer are Copyright (C) 2006
  19.  * the Initial Developer. All Rights Reserved.
  20.  *
  21.  * Contributor(s):
  22.  *   Asaf Romano <mano@mozilla.com>
  23.  *
  24.  * Alternatively, the contents of this file may be used under the terms of
  25.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  26.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  27.  * in which case the provisions of the GPL or the LGPL are applicable instead
  28.  * of those above. If you wish to allow use of your version of this file only
  29.  * under the terms of either the GPL or the LGPL, and not to allow others to
  30.  * use your version of this file under the terms of the MPL, indicate your
  31.  * decision by deleting the provisions above and replace them with the notice
  32.  * and other provisions required by the GPL or the LGPL. If you do not delete
  33.  * the provisions above, a recipient may use your version of this file under
  34.  * the terms of any one of the MPL, the GPL or the LGPL.
  35.  *
  36.  * ***** END LICENSE BLOCK ***** */
  37.  
  38. const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed";
  39. const STATIC_TITLE_ANNO = "bookmarks/staticTitle";
  40. const MAX_FOLDER_ITEM_IN_MENU_LIST = 5;
  41.  
  42. var gEditItemOverlay = {
  43.   _uri: null,
  44.   _itemId: -1,
  45.   _itemType: -1,
  46.   _readOnly: false,
  47.   _microsummaries: null,
  48.   _hiddenRows: [],
  49.   _observersAdded: false,
  50.   _staticFoldersListBuilt: false,
  51.  
  52.   get itemId() {
  53.     return this._itemId;
  54.   },
  55.  
  56.   /**
  57.    * Determines the initial data for the item edited or added by this dialog
  58.    */
  59.   _determineInfo: function EIO__determineInfo(aInfo) {
  60.     // hidden rows
  61.     if (aInfo && aInfo.hiddenRows)
  62.       this._hiddenRows = aInfo.hiddenRows;
  63.     else
  64.       this._hiddenRows.splice(0);
  65.     // force-read-only
  66.     this._readOnly = aInfo && aInfo.forceReadOnly;
  67.   },
  68.  
  69.   _showHideRows: function EIO__showHideRows() {
  70.     var isBookmark = this._itemId != -1 &&
  71.                      this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK;
  72.     var isQuery = false;
  73.     if (this._uri)
  74.       isQuery = this._uri.schemeIs("place");
  75.  
  76.     this._element("nameRow").collapsed = this._hiddenRows.indexOf("name") != -1;
  77.     this._element("folderRow").collapsed =
  78.       this._hiddenRows.indexOf("folderPicker") != -1 || this._readOnly;
  79.  
  80.     this._element("tagsRow").collapsed = !this._uri ||
  81.       this._hiddenRows.indexOf("tags") != -1 || isQuery;
  82.     this._element("descriptionRow").collapsed =
  83.       this._hiddenRows.indexOf("description") != -1 || this._readOnly;
  84.     this._element("keywordRow").collapsed = !isBookmark || this._readOnly ||
  85.       this._hiddenRows.indexOf("keyword") != -1 || isQuery;
  86.     this._element("locationRow").collapsed = !isBookmark || isQuery ||
  87.       this._hiddenRows.indexOf("location") != -1;
  88.     this._element("loadInSidebarCheckbox").collapsed = !isBookmark || isQuery ||
  89.       this._readOnly || this._hiddenRows.indexOf("loadInSidebar") != -1;
  90.     this._element("feedLocationRow").collapsed = !this._isLivemark ||
  91.       this._hiddenRows.indexOf("feedLocation") != -1;
  92.     this._element("siteLocationRow").collapsed = !this._isLivemark ||
  93.       this._hiddenRows.indexOf("siteLocation") != -1;
  94.   },
  95.  
  96.   /**
  97.    * Initialize the panel
  98.    * @param aFor
  99.    *        Either a places-itemId (of a bookmark, folder or a live bookmark),
  100.    *        or a URI object (in which case, the panel would be initialized in
  101.    *        read-only mode).
  102.    * @param [optional] aInfo
  103.    *        JS object which stores additional info for the panel
  104.    *        initialization. The following properties may bet set:
  105.    *        * hiddenRows (Strings array): list of rows to be hidden regardless
  106.    *          of the item edited. Possible values: "title", "location",
  107.    *          "description", "keyword", "loadInSidebar", "feedLocation",
  108.    *          "siteLocation", folderPicker"
  109.    *        * forceReadOnly - set this flag to initialize the panel to its
  110.    *          read-only (view) mode even if the given item is editable.
  111.    */
  112.   initPanel: function EIO_initPanel(aFor, aInfo) {
  113.     this._folderMenuList = this._element("folderMenuList");
  114.     this._folderTree = this._element("folderTree");
  115.  
  116.     this._determineInfo(aInfo);
  117.     if (aFor instanceof Ci.nsIURI) {
  118.       this._itemId = -1;
  119.       this._uri = aFor;
  120.       this._readOnly = true;
  121.     }
  122.     else {
  123.       this._itemId = aFor;
  124.       var container =  PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
  125.       this._itemType = PlacesUtils.bookmarks.getItemType(this._itemId);
  126.       if (this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
  127.         this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
  128.         if (!this._readOnly) // If readOnly wasn't forced through aInfo
  129.           this._readOnly = PlacesUtils.livemarks.isLivemark(container);
  130.         this._initTextField("keywordField",
  131.                             PlacesUtils.bookmarks
  132.                                        .getKeywordForBookmark(this._itemId));
  133.         // Load In Sidebar checkbox
  134.         this._element("loadInSidebarCheckbox").checked =
  135.           PlacesUtils.annotations.itemHasAnnotation(this._itemId,
  136.                                                     LOAD_IN_SIDEBAR_ANNO);
  137.       }
  138.       else {
  139.         if (!this._readOnly) // If readOnly wasn't forced through aInfo
  140.           this._readOnly = false;
  141.  
  142.         this._uri = null;
  143.         this._isLivemark = PlacesUtils.livemarks.isLivemark(this._itemId);
  144.         if (this._isLivemark) {
  145.           var feedURI = PlacesUtils.livemarks.getFeedURI(this._itemId);
  146.           var siteURI = PlacesUtils.livemarks.getSiteURI(this._itemId);
  147.           this._initTextField("feedLocationField", feedURI.spec);
  148.           this._initTextField("siteLocationField", siteURI ? siteURI.spec : "");
  149.         }
  150.       }
  151.  
  152.       // folder picker
  153.       this._initFolderMenuList(container);
  154.  
  155.       // description field
  156.       this._initTextField("descriptionField", 
  157.                           PlacesUIUtils.getItemDescription(this._itemId));
  158.     }
  159.  
  160.     if (this._itemId == -1 ||
  161.         this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
  162.       this._isLivemark = false;
  163.  
  164.       this._initTextField("locationField", this._uri.spec);
  165.       this._initTextField("tagsField",
  166.                            PlacesUtils.tagging
  167.                                       .getTagsForURI(this._uri, {}).join(", "),
  168.                           false);
  169.  
  170.       // tags selector
  171.       this._rebuildTagsSelectorList();
  172.     }
  173.  
  174.     // name picker
  175.     this._initNamePicker();
  176.     
  177.     this._showHideRows();
  178.  
  179.     // observe changes
  180.     if (!this._observersAdded) {
  181.       if (this._itemId != -1)
  182.         PlacesUtils.bookmarks.addObserver(this, false);
  183.       window.addEventListener("unload", this, false);
  184.       this._observersAdded = true;
  185.     }
  186.   },
  187.  
  188.   _initTextField: function(aTextFieldId, aValue, aReadOnly) {
  189.     var field = this._element(aTextFieldId);
  190.     field.readOnly = aReadOnly !== undefined ? aReadOnly : this._readOnly;
  191.  
  192.     if (field.value != aValue) {
  193.       field.value = aValue;
  194.  
  195.       // clear the undo stack
  196.       var editor = field.editor;
  197.       if (editor)
  198.         editor.transactionManager.clear();
  199.     }
  200.   },
  201.  
  202.   /**
  203.    * Appends a menu-item representing a bookmarks folder to a menu-popup.
  204.    * @param aMenupopup
  205.    *        The popup to which the menu-item should be added.
  206.    * @param aFolderId
  207.    *        The identifier of the bookmarks folder.
  208.    * @return the new menu item.
  209.    */
  210.   _appendFolderItemToMenupopup:
  211.   function EIO__appendFolderItemToMenuList(aMenupopup, aFolderId) {
  212.     // First make sure the folders-separator is visible
  213.     this._element("foldersSeparator").hidden = false;
  214.  
  215.     var folderMenuItem = document.createElement("menuitem");
  216.     var folderTitle = PlacesUtils.bookmarks.getItemTitle(aFolderId)
  217.     folderMenuItem.folderId = aFolderId;
  218.     folderMenuItem.setAttribute("label", folderTitle);
  219.     folderMenuItem.className = "menuitem-iconic folder-icon";
  220.     aMenupopup.appendChild(folderMenuItem);
  221.     return folderMenuItem;
  222.   },
  223.  
  224.   _initFolderMenuList: function EIO__initFolderMenuList(aSelectedFolder) {
  225.     // clean up first
  226.     var menupopup = this._folderMenuList.menupopup;
  227.     while (menupopup.childNodes.length > 6)
  228.       menupopup.removeChild(menupopup.lastChild);
  229.  
  230.     const bms = PlacesUtils.bookmarks;
  231.     const annos = PlacesUtils.annotations;
  232.  
  233.     // Build the static list
  234.     var unfiledItem = this._element("unfiledRootItem");
  235.     if (!this._staticFoldersListBuilt) {
  236.       unfiledItem.label = bms.getItemTitle(PlacesUtils.unfiledBookmarksFolderId);
  237.       unfiledItem.folderId = PlacesUtils.unfiledBookmarksFolderId;
  238.       var bmMenuItem = this._element("bmRootItem");
  239.       bmMenuItem.label = bms.getItemTitle(PlacesUtils.bookmarksMenuFolderId);
  240.       bmMenuItem.folderId = PlacesUtils.bookmarksMenuFolderId;
  241.       var toolbarItem = this._element("toolbarFolderItem");
  242.       toolbarItem.label = bms.getItemTitle(PlacesUtils.toolbarFolderId);
  243.       toolbarItem.folderId = PlacesUtils.toolbarFolderId;
  244.       this._staticFoldersListBuilt = true;
  245.     }
  246.  
  247.     // List of recently used folders:
  248.     var folderIds = annos.getItemsWithAnnotation(LAST_USED_ANNO, { });
  249.  
  250.     /**
  251.      * The value of the LAST_USED_ANNO annotation is the time (in the form of
  252.      * Date.getTime) at which the folder has been last used.
  253.      *
  254.      * First we build the annotated folders array, each item has both the
  255.      * folder identifier and the time at which it was last-used by this dialog
  256.      * set. Then we sort it descendingly based on the time field.
  257.      */
  258.     var folders = [];
  259.     for (var i=0; i < folderIds.length; i++) {
  260.       var lastUsed = annos.getItemAnnotation(folderIds[i], LAST_USED_ANNO);
  261.       folders.push({ folderId: folderIds[i], lastUsed: lastUsed });
  262.     }
  263.     folders.sort(function(a, b) {
  264.       if (b.lastUsed < a.lastUsed)
  265.         return -1;
  266.       if (b.lastUsed > a.lastUsed)
  267.         return 1;
  268.       return 0;
  269.     });
  270.  
  271.     var numberOfItems = Math.min(MAX_FOLDER_ITEM_IN_MENU_LIST, folders.length);
  272.     for (i=0; i < numberOfItems; i++) {
  273.       this._appendFolderItemToMenupopup(menupopup, folders[i].folderId);
  274.     }
  275.  
  276.     var defaultItem = this._getFolderMenuItem(aSelectedFolder);
  277.     this._folderMenuList.selectedItem = defaultItem;
  278.  
  279.     // Hide the folders-separator if no folder is annotated as recently-used
  280.     this._element("foldersSeparator").hidden = (menupopup.childNodes.length <= 6);
  281.     this._folderMenuList.disabled = this._readOnly;
  282.   },
  283.  
  284.   QueryInterface: function EIO_QueryInterface(aIID) {
  285.     if (aIID.equals(Ci.nsIMicrosummaryObserver) ||
  286.         aIID.equals(Ci.nsIDOMEventListener) ||
  287.         aIID.equals(Ci.nsINavBookmarkObserver) ||
  288.         aIID.equals(Ci.nsISupports))
  289.       return this;
  290.  
  291.     throw Cr.NS_ERROR_NO_INTERFACE;
  292.   },
  293.  
  294.   _element: function EIO__element(aID) {
  295.     return document.getElementById("editBMPanel_" + aID);
  296.   },
  297.  
  298.   _createMicrosummaryMenuItem:
  299.   function EIO__createMicrosummaryMenuItem(aMicrosummary) {
  300.     var menuItem = document.createElement("menuitem");
  301.  
  302.     // Store a reference to the microsummary in the menu item, so we know
  303.     // which microsummary this menu item represents when it's time to
  304.     // save changes or load its content.
  305.     menuItem.microsummary = aMicrosummary;
  306.  
  307.     // Content may have to be generated asynchronously; we don't necessarily
  308.     // have it now.  If we do, great; otherwise, fall back to the generator
  309.     // name, then the URI, and we trigger a microsummary content update. Once
  310.     // the update completes, the microsummary will notify our observer to
  311.     // update the corresponding menu-item.
  312.     // XXX Instead of just showing the generator name or (heaven forbid)
  313.     // its URI when we don't have content, we should tell the user that
  314.     // we're loading the microsummary, perhaps with some throbbing to let
  315.     // her know it is in progress.
  316.     if (aMicrosummary.content)
  317.       menuItem.setAttribute("label", aMicrosummary.content);
  318.     else {
  319.       menuItem.setAttribute("label", aMicrosummary.generator.name ||
  320.                                      aMicrosummary.generator.uri.spec);
  321.       aMicrosummary.update();
  322.     }
  323.  
  324.     return menuItem;
  325.   },
  326.  
  327.   _getItemStaticTitle: function EIO__getItemStaticTitle() {
  328.     if (this._itemId == -1)
  329.       return PlacesUtils.history.getPageTitle(this._uri);
  330.  
  331.     const annos = PlacesUtils.annotations;
  332.     if (annos.itemHasAnnotation(this._itemId, STATIC_TITLE_ANNO))
  333.       return annos.getItemAnnotation(this._itemId, STATIC_TITLE_ANNO);
  334.  
  335.     return PlacesUtils.bookmarks.getItemTitle(this._itemId);
  336.   },
  337.  
  338.   _initNamePicker: function EIO_initNamePicker() {
  339.     var userEnteredNameField = this._element("userEnteredName");
  340.     var namePicker = this._element("namePicker");
  341.     var droppable = false;
  342.  
  343.     userEnteredNameField.label = this._getItemStaticTitle();
  344.  
  345.     // clean up old entries
  346.     var menupopup = namePicker.menupopup;
  347.     while (menupopup.childNodes.length > 2)
  348.       menupopup.removeChild(menupopup.lastChild);
  349.  
  350.     if (this._microsummaries) {
  351.       this._microsummaries.removeObserver(this);
  352.       this._microsummaries = null;
  353.     }
  354.  
  355.     var itemToSelect = userEnteredNameField;
  356.     try {
  357.       if (this._itemId != -1 &&
  358.           this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK &&
  359.           !this._readOnly)
  360.         this._microsummaries = PlacesUIUtils.microsummaries
  361.                                             .getMicrosummaries(this._uri, -1);
  362.     }
  363.     catch(ex) {
  364.       // getMicrosummaries will throw an exception in at least two cases:
  365.       // 1. the bookmarked URI contains a scheme that the service won't
  366.       //    download for security reasons (currently it only handles http,
  367.       //    https, and file);
  368.       // 2. the page to which the URI refers isn't HTML or XML (the only two
  369.       //    content types the service knows how to summarize).
  370.       this._microsummaries = null;
  371.     }
  372.     if (this._microsummaries) {
  373.       var enumerator = this._microsummaries.Enumerate();
  374.  
  375.       if (enumerator.hasMoreElements()) {
  376.         // Show the drop marker if there are microsummaries
  377.         droppable = true;
  378.         while (enumerator.hasMoreElements()) {
  379.           var microsummary = enumerator.getNext()
  380.                                        .QueryInterface(Ci.nsIMicrosummary);
  381.           var menuItem = this._createMicrosummaryMenuItem(microsummary);
  382.           if (PlacesUIUtils.microsummaries
  383.                            .isMicrosummary(this._itemId, microsummary))
  384.             itemToSelect = menuItem;
  385.  
  386.           menupopup.appendChild(menuItem);
  387.         }
  388.       }
  389.  
  390.       this._microsummaries.addObserver(this);
  391.     }
  392.  
  393.     if (namePicker.selectedItem == itemToSelect)
  394.       namePicker.value = itemToSelect.label;
  395.     else
  396.       namePicker.selectedItem = itemToSelect;
  397.  
  398.     namePicker.setAttribute("droppable", droppable);
  399.     namePicker.readOnly = this._readOnly;
  400.  
  401.     // clear the undo stack
  402.     var editor = namePicker.editor;
  403.     if (editor)
  404.       editor.transactionManager.clear();
  405.   },
  406.  
  407.   // nsIMicrosummaryObserver
  408.   onContentLoaded: function EIO_onContentLoaded(aMicrosummary) {
  409.     var namePicker = this._element("namePicker");
  410.     var childNodes = namePicker.menupopup.childNodes;
  411.  
  412.     // 0: user-entered item; 1: separator
  413.     for (var i = 2; i < childNodes.length; i++) {
  414.       if (childNodes[i].microsummary == aMicrosummary) {
  415.         var newLabel = aMicrosummary.content;
  416.         // XXXmano: non-editable menulist would do this for us, see bug 360220
  417.         // We should fix editable-menulists to set the DOMAttrModified handler
  418.         // as well.
  419.         //
  420.         // Also note the order importance: if the label of the menu-item is
  421.         // set to something different than the menulist's current value,
  422.         // the menulist no longer has selectedItem set
  423.         if (namePicker.selectedItem == childNodes[i])
  424.           namePicker.value = newLabel;
  425.  
  426.         childNodes[i].label = newLabel;
  427.         return;
  428.       }
  429.     }
  430.   },
  431.  
  432.   onElementAppended: function EIO_onElementAppended(aMicrosummary) {
  433.     var namePicker = this._element("namePicker");
  434.     namePicker.menupopup
  435.               .appendChild(this._createMicrosummaryMenuItem(aMicrosummary));
  436.  
  437.     // Make sure the drop-marker is shown
  438.     namePicker.setAttribute("droppable", "true");
  439.   },
  440.  
  441.   uninitPanel: function EIO_uninitPanel(aHideCollapsibleElements) {
  442.     if (aHideCollapsibleElements) {
  443.       // hide the folder tree if it was previously visible
  444.       if (!this._folderTree.collapsed)
  445.         this.toggleFolderTreeVisibility();
  446.  
  447.       // hide the tag selector if it was previously visible
  448.       var tagsSelector = this._element("tagsSelector");
  449.       if (!tagsSelector.collapsed)
  450.         this.toggleTagsSelector();
  451.     }
  452.  
  453.     if (this._observersAdded) {
  454.       if (this._itemId != -1)
  455.         PlacesUtils.bookmarks.removeObserver(this);
  456.  
  457.       this._observersAdded = false;
  458.     }
  459.     if (this._microsummaries) {
  460.       this._microsummaries.removeObserver(this);
  461.       this._microsummaries = null;
  462.     }
  463.     this._itemId = -1;
  464.     this._uri = null;
  465.   },
  466.  
  467.   onTagsFieldBlur: function EIO_onTagsFieldBlur() {
  468.     this._updateTags();
  469.   },
  470.  
  471.   _updateTags: function EIO__updateTags() {
  472.     var currentTags = PlacesUtils.tagging.getTagsForURI(this._uri, { });
  473.     var tags = this._getTagsArrayFromTagField();
  474.     if (tags.length > 0 || currentTags.length > 0) {
  475.       var tagsToRemove = [];
  476.       var tagsToAdd = [];
  477.       var i;
  478.       for (i = 0; i < currentTags.length; i++) {
  479.         if (tags.indexOf(currentTags[i]) == -1)
  480.           tagsToRemove.push(currentTags[i]);
  481.       }
  482.       for (i = 0; i < tags.length; i++) {
  483.         if (currentTags.indexOf(tags[i]) == -1)
  484.           tagsToAdd.push(tags[i]);
  485.       }
  486.  
  487.       if (tagsToAdd.length > 0) {
  488.         var tagTxn = PlacesUIUtils.ptm.tagURI(this._uri, tagsToAdd);
  489.         PlacesUIUtils.ptm.doTransaction(tagTxn);
  490.       }
  491.       if (tagsToRemove.length > 0) {
  492.         var untagTxn = PlacesUIUtils.ptm.untagURI(this._uri, tagsToRemove);
  493.         PlacesUIUtils.ptm.doTransaction(untagTxn);
  494.       }
  495.     }
  496.   },
  497.  
  498.   onNamePickerInput: function EIO_onNamePickerInput() {
  499.     var title = this._element("namePicker").value;
  500.     this._element("userEnteredName").label = title;
  501.   },
  502.  
  503.   onNamePickerChange: function EIO_onNamePickerChange() {
  504.     if (this._itemId == -1)
  505.       return;
  506.  
  507.     var namePicker = this._element("namePicker")
  508.     var txns = [];
  509.     const ptm = PlacesUIUtils.ptm;
  510.  
  511.     // Here we update either the item title or its cached static title
  512.     var newTitle = this._element("userEnteredName").label;
  513.     if (this._getItemStaticTitle() != newTitle) {
  514.       if (PlacesUIUtils.microsummaries.hasMicrosummary(this._itemId)) {
  515.         // Note: this implicitly also takes care of the microsummary->static
  516.         // title case, the removeMicorosummary method in the service will set
  517.         // the item-title to the value of this annotation.
  518.         //
  519.         // XXXmano: use a transaction
  520.         PlacesUtils.setAnnotationsForItem(this._itemId,
  521.                                           [{name: STATIC_TITLE_ANNO,
  522.                                             value: newTitle}]);
  523.       }
  524.       else
  525.         txns.push(ptm.editItemTitle(this._itemId, newTitle));
  526.     }
  527.  
  528.     var newMicrosummary = namePicker.selectedItem.microsummary;
  529.  
  530.     // Only add a microsummary update to the transaction if the microsummary
  531.     // has actually changed, i.e. the user selected no microsummary, but the
  532.     // bookmark previously had one, or the user selected a microsummary which
  533.     // is not the one the bookmark previously had
  534.     if ((newMicrosummary == null &&
  535.          PlacesUIUtils.microsummaries.hasMicrosummary(this._itemId)) ||
  536.         (newMicrosummary != null &&
  537.          !PlacesUIUtils.microsummaries
  538.                        .isMicrosummary(this._itemId, newMicrosummary))) {
  539.       txns.push(ptm.editBookmarkMicrosummary(this._itemId, newMicrosummary));
  540.     }
  541.  
  542.     var aggregate = ptm.aggregateTransactions("Edit Item Title", txns);
  543.     ptm.doTransaction(aggregate);
  544.   },
  545.  
  546.   onDescriptionFieldBlur: function EIO_onDescriptionFieldInput() {
  547.     var description = this._element("descriptionField").value;
  548.     if (description != PlacesUIUtils.getItemDescription(this._itemId)) {
  549.       var txn = PlacesUIUtils.ptm
  550.                              .editItemDescription(this._itemId, description);
  551.       PlacesUIUtils.ptm.doTransaction(txn);
  552.     }
  553.   },
  554.  
  555.   onLocationFieldBlur: function EIO_onLocationFieldBlur() {
  556.     var uri;
  557.     try {
  558.       uri = PlacesUIUtils.createFixedURI(this._element("locationField").value);
  559.     }
  560.     catch(ex) { return; }
  561.  
  562.     if (!this._uri.equals(uri)) {
  563.       var txn = PlacesUIUtils.ptm.editBookmarkURI(this._itemId, uri);
  564.       PlacesUIUtils.ptm.doTransaction(txn);
  565.     }
  566.   },
  567.  
  568.   onKeywordFieldBlur: function EIO_onKeywordFieldBlur() {
  569.     var keyword = this._element("keywordField").value;
  570.     if (keyword != PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId)) {
  571.       var txn = PlacesUIUtils.ptm.editBookmarkKeyword(this._itemId, keyword);
  572.       PlacesUIUtils.ptm.doTransaction(txn);
  573.     }
  574.   },
  575.  
  576.   onFeedLocationFieldBlur: function EIO_onFeedLocationFieldBlur() {
  577.     var uri;
  578.     try {
  579.       uri = PlacesUIUtils.createFixedURI(this._element("feedLocationField").value);
  580.     }
  581.     catch(ex) { return; }
  582.  
  583.     var currentFeedURI = PlacesUtils.livemarks.getFeedURI(this._itemId);
  584.     if (!currentFeedURI.equals(uri)) {
  585.       var txn = PlacesUIUtils.ptm.editLivemarkFeedURI(this._itemId, uri);
  586.       PlacesUIUtils.ptm.doTransaction(txn);
  587.     }
  588.   },
  589.  
  590.   onSiteLocationFieldBlur: function EIO_onSiteLocationFieldBlur() {
  591.     var uri = null;
  592.     try {
  593.       uri = PlacesUIUtils.createFixedURI(this._element("siteLocationField").value);
  594.     }
  595.     catch(ex) {  }
  596.  
  597.     var currentSiteURI = PlacesUtils.livemarks.getSiteURI(this._itemId);
  598.     if (!uri || !currentSiteURI.equals(uri)) {
  599.       var txn = PlacesUIUtils.ptm.editLivemarkSiteURI(this._itemId, uri);
  600.       PlacesUIUtils.ptm.doTransaction(txn);
  601.     }
  602.   },
  603.  
  604.   onLoadInSidebarCheckboxCommand:
  605.   function EIO_onLoadInSidebarCheckboxCommand() {
  606.     var loadInSidebarChecked = this._element("loadInSidebarCheckbox").checked;
  607.     var txn = PlacesUIUtils.ptm.setLoadInSidebar(this._itemId,
  608.                                                  loadInSidebarChecked);
  609.     PlacesUIUtils.ptm.doTransaction(txn);
  610.   },
  611.  
  612.   toggleFolderTreeVisibility: function EIO_toggleFolderTreeVisibility() {
  613.     var expander = this._element("foldersExpander");
  614.     if (!this._folderTree.collapsed) {
  615.       expander.className = "expander-down";
  616.       expander.setAttribute("tooltiptext",
  617.                             expander.getAttribute("tooltiptextdown"));
  618.       this._folderTree.collapsed =
  619.         this._element("newFolderBox").collapsed = true;
  620.       this._element("chooseFolderSeparator").hidden =
  621.         this._element("chooseFolderMenuItem").hidden = false;
  622.     }
  623.     else {
  624.       expander.className = "expander-up"
  625.       expander.setAttribute("tooltiptext",
  626.                             expander.getAttribute("tooltiptextup"));
  627.       this._folderTree.collapsed =
  628.         this._element("newFolderBox").collapsed = false;
  629.  
  630.       // XXXmano: Ideally we would only do this once, but for some odd reason,
  631.       // the editable mode set on this tree, together with its collapsed state
  632.       // breaks the view.
  633.       const FOLDER_TREE_PLACE_URI =
  634.         "place:excludeItems=1&excludeQueries=1&excludeReadOnlyFolders=1&folder=" +
  635.         window.top.PlacesUIUtils.allBookmarksFolderId;
  636.       this._folderTree.place = FOLDER_TREE_PLACE_URI;
  637.  
  638.       this._element("chooseFolderSeparator").hidden =
  639.         this._element("chooseFolderMenuItem").hidden = true;
  640.       var currentFolder = this._getFolderIdFromMenuList();
  641.       this._folderTree.selectItems([currentFolder]);
  642.       this._folderTree.focus();
  643.     }
  644.   },
  645.  
  646.   _getFolderIdFromMenuList:
  647.   function EIO__getFolderIdFromMenuList() {
  648.     var selectedItem = this._folderMenuList.selectedItem;
  649.     NS_ASSERT("folderId" in selectedItem,
  650.               "Invalid menuitem in the folders-menulist");
  651.     return selectedItem.folderId;
  652.   },
  653.  
  654.   /**
  655.    * Get the corresponding menu-item in the folder-menu-list for a bookmarks
  656.    * folder if such an item exists. Otherwise, this creates a menu-item for the
  657.    * folder. If the items-count limit (see MAX_FOLDERS_IN_MENU_LIST) is reached,
  658.    * the new item replaces the last menu-item.
  659.    * @param aFolderId
  660.    *        The identifier of the bookmarks folder.
  661.    */
  662.   _getFolderMenuItem:
  663.   function EIO__getFolderMenuItem(aFolderId) {
  664.     var menupopup = this._folderMenuList.menupopup;
  665.  
  666.     for (var i=0;  i < menupopup.childNodes.length; i++) {
  667.       if (menupopup.childNodes[i].folderId == aFolderId)
  668.         return menupopup.childNodes[i];
  669.     }
  670.  
  671.     // 3 special folders + separator + folder-items-count limit
  672.     if (menupopup.childNodes.length == 4 + MAX_FOLDER_ITEM_IN_MENU_LIST)
  673.       menupopup.removeChild(menupopup.lastChild);
  674.  
  675.     return this._appendFolderItemToMenupopup(menupopup, aFolderId);
  676.   },
  677.  
  678.   onFolderMenuListCommand: function EIO_onFolderMenuListCommand(aEvent) {
  679.     if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") {
  680.       // reset the selection back to where it was and expand the tree
  681.       // (this menu-item is hidden when the tree is already visible
  682.       var container = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
  683.       var item = this._getFolderMenuItem(container);
  684.       this._folderMenuList.selectedItem = item;
  685.       // XXXmano HACK: setTimeout 100, otherwise focus goes back to the
  686.       // menulist right away
  687.       setTimeout(function(self) self.toggleFolderTreeVisibility(), 100, this);
  688.       return;
  689.     }
  690.  
  691.     // Move the item
  692.     var container = this._getFolderIdFromMenuList();
  693.     if (PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) != container) {
  694.       var txn = PlacesUIUtils.ptm.moveItem(this._itemId, container, -1);
  695.       PlacesUIUtils.ptm.doTransaction(txn);
  696.  
  697.       // Mark the containing folder as recently-used if it isn't in the
  698.       // static list
  699.       if (container != PlacesUtils.unfiledBookmarksFolderId &&
  700.           container != PlacesUtils.toolbarFolderId &&
  701.           container != PlacesUtils.bookmarksMenuFolderId)
  702.         this._markFolderAsRecentlyUsed(container);
  703.     }
  704.  
  705.     // Update folder-tree selection
  706.     if (!this._folderTree.collapsed) {
  707.       var selectedNode = this._folderTree.selectedNode;
  708.       if (!selectedNode ||
  709.           PlacesUtils.getConcreteItemId(selectedNode) != container)
  710.         this._folderTree.selectItems([container]);
  711.     }
  712.   },
  713.  
  714.   onFolderTreeSelect: function EIO_onFolderTreeSelect() {
  715.     var selectedNode = this._folderTree.selectedNode;
  716.     if (!selectedNode)
  717.       return;
  718.  
  719.     var folderId = PlacesUtils.getConcreteItemId(selectedNode);
  720.     if (this._getFolderIdFromMenuList() == folderId)
  721.       return;
  722.  
  723.     var folderItem = this._getFolderMenuItem(folderId);
  724.     this._folderMenuList.selectedItem = folderItem;
  725.     folderItem.doCommand();
  726.   },
  727.  
  728.   _markFolderAsRecentlyUsed:
  729.   function EIO__markFolderAsRecentlyUsed(aFolderId) {
  730.     // We'll figure out when/if to expire the annotation if it turns out
  731.     // we keep this recently-used-folders implementation
  732.     PlacesUtils.annotations
  733.                .setItemAnnotation(aFolderId, LAST_USED_ANNO,
  734.                                   new Date().getTime(), 0,
  735.                                   Ci.nsIAnnotationService.EXPIRE_NEVER);
  736.   },
  737.  
  738.   _rebuildTagsSelectorList: function EIO__rebuildTagsSelectorList() {
  739.     var tagsSelector = this._element("tagsSelector");
  740.     if (tagsSelector.collapsed)
  741.       return;
  742.  
  743.     while (tagsSelector.hasChildNodes())
  744.       tagsSelector.removeChild(tagsSelector.lastChild);
  745.  
  746.     var tagsInField = this._getTagsArrayFromTagField();
  747.     var allTags = PlacesUtils.tagging.allTags;
  748.     for (var i = 0; i < allTags.length; i++) {
  749.       var tag = allTags[i];
  750.       var elt = document.createElement("listitem");
  751.       elt.setAttribute("type", "checkbox");
  752.       elt.setAttribute("label", tag);
  753.       if (tagsInField.indexOf(tag) != -1)
  754.         elt.setAttribute("checked", "true");
  755.  
  756.       tagsSelector.appendChild(elt);
  757.     }
  758.   },
  759.  
  760.   toggleTagsSelector: function EIO_toggleTagsSelector() {
  761.     var tagsSelector = this._element("tagsSelector");
  762.     var expander = this._element("tagsSelectorExpander");
  763.     if (tagsSelector.collapsed) {
  764.       expander.className = "expander-up";
  765.       expander.setAttribute("tooltiptext",
  766.                             expander.getAttribute("tooltiptextup"));
  767.       tagsSelector.collapsed = false;
  768.       this._rebuildTagsSelectorList();
  769.  
  770.       // This is a no-op if we've added the listener.
  771.       tagsSelector.addEventListener("CheckboxStateChange", this, false);
  772.     }
  773.     else {
  774.       expander.className = "expander-down";
  775.       expander.setAttribute("tooltiptext",
  776.                             expander.getAttribute("tooltiptextdown"));
  777.       tagsSelector.collapsed = true;
  778.     }
  779.   },
  780.  
  781.   _getTagsArrayFromTagField: function EIO__getTagsArrayFromTagField() {
  782.     // we don't require the leading space (after each comma)
  783.     var tags = this._element("tagsField").value.split(",");
  784.     for (var i=0; i < tags.length; i++) {
  785.       // remove trailing and leading spaces
  786.       tags[i] = tags[i].replace(/^\s+/, "").replace(/\s+$/, "");
  787.  
  788.       // remove empty entries from the array.
  789.       if (tags[i] == "") {
  790.         tags.splice(i, 1);
  791.         i--;
  792.       }
  793.     }
  794.     return tags;
  795.   },
  796.  
  797.   newFolder: function EIO_newFolder() {
  798.     var ip = this._folderTree.insertionPoint;
  799.  
  800.     // default to the bookmarks menu folder
  801.     if (!ip ||
  802.         ip.itemId == PlacesUIUtils.allBookmarksFolderId ||
  803.         ip.itemId == PlacesUIUtils.unfiledBookmarksFolderId) {
  804.       ip.itemId = PlacesUtils.bookmarksMenuFolderId;
  805.       ip.index = -1;
  806.     }
  807.  
  808.     // XXXmano: add a separate "New Folder" string at some point...
  809.     var defaultLabel = this._element("newFolderButton").label;
  810.     var txn = PlacesUIUtils.ptm.createFolder(defaultLabel, ip.itemId, ip.index);
  811.     PlacesUIUtils.ptm.doTransaction(txn);
  812.     this._folderTree.focus();
  813.     this._folderTree.selectItems([this._lastNewItem]);
  814.     this._folderTree.startEditing(this._folderTree.view.selection.currentIndex,
  815.                                   this._folderTree.columns.getFirstColumn());
  816.   },
  817.  
  818.   // nsIDOMEventListener
  819.   handleEvent: function EIO_nsIDOMEventListener(aEvent) {
  820.     switch (aEvent.type) {
  821.     case "CheckboxStateChange":
  822.       // Update the tags field when items are checked/unchecked in the listbox
  823.       var tags = this._getTagsArrayFromTagField();
  824.  
  825.       if (aEvent.target.checked)
  826.         tags.push(aEvent.target.label);
  827.       else {
  828.         var indexOfItem = tags.indexOf(aEvent.target.label);
  829.         if (indexOfItem != -1)
  830.           tags.splice(indexOfItem, 1);
  831.       }
  832.       this._element("tagsField").value = tags.join(", ");
  833.       this._updateTags();
  834.       break;
  835.     case "unload":
  836.       this.uninitPanel(false);
  837.       break;
  838.     }
  839.   },
  840.  
  841.   // nsINavBookmarkObserver
  842.   onItemChanged: function EIO_onItemChanged(aItemId, aProperty,
  843.                                             aIsAnnotationProperty, aValue) {
  844.     if (this._itemId != aItemId) {
  845.       if (aProperty == "title") {
  846.         // If the title of a folder which is listed within the folders
  847.         // menulist has been changed, we need to update the label of its
  848.         // representing element.
  849.         var menupopup = this._folderMenuList.menupopup;
  850.         for (var i=0; i < menupopup.childNodes.length; i++) {
  851.           if (menupopup.childNodes[i].folderId == aItemId) {
  852.             menupopup.childNodes[i].label = aValue;
  853.             break;
  854.           }
  855.         }
  856.       }
  857.  
  858.       return;
  859.     }
  860.  
  861.     switch (aProperty) {
  862.     case "title":
  863.       if (PlacesUtils.annotations.itemHasAnnotation(this._itemId,
  864.                                                     STATIC_TITLE_ANNO))
  865.         return;  // onContentLoaded updates microsummary-items
  866.  
  867.       var userEnteredNameField = this._element("userEnteredName");
  868.       if (userEnteredNameField.value != aValue) {
  869.         userEnteredNameField.value = aValue;
  870.         var namePicker = this._element("namePicker");
  871.         if (namePicker.selectedItem == userEnteredNameField) {
  872.           namePicker.label = aValue;
  873.  
  874.           // clear undo stack
  875.           namePicker.editor.transactionManager.clear();
  876.         }
  877.       }
  878.       break;
  879.     case "uri":
  880.       var locationField = this._element("locationField");
  881.       if (locationField.value != aValue) {
  882.         this._uri = Cc["@mozilla.org/network/io-service;1"].
  883.                     getService(Ci.nsIIOService).
  884.                     newURI(aValue, null, null);
  885.         this._initTextField("locationField", this._uri.spec);
  886.         this._initNamePicker(); // for microsummaries
  887.         this._initTextField("tagsField",
  888.                              PlacesUtils.tagging
  889.                                         .getTagsForURI(this._uri, { }).join(", "),
  890.                             false);
  891.         this._rebuildTagsSelectorList();
  892.       }
  893.       break;
  894.     case "keyword":
  895.       this._initTextField("keywordField",
  896.                           PlacesUtils.bookmarks
  897.                                      .getKeywordForBookmark(this._itemId));
  898.       break;
  899.     case DESCRIPTION_ANNO:
  900.       this._initTextField("descriptionField",
  901.                           PlacesUIUtils.getItemDescription(this._itemId));
  902.       break;
  903.     case LOAD_IN_SIDEBAR_ANNO:
  904.       this._element("loadInSidebarCheckbox").checked =
  905.         PlacesUtils.annotations.itemHasAnnotation(this._itemId,
  906.                                                   LOAD_IN_SIDEBAR_ANNO);
  907.       break;
  908.     case LMANNO_FEEDURI:
  909.       var feedURISpec = PlacesUtils.livemarks.getFeedURI(this._itemId).spec;
  910.       this._initTextField("feedLocationField", feedURISpec);
  911.       break;
  912.     case LMANNO_SITEURI:
  913.       var siteURISpec = "";
  914.       var siteURI = PlacesUtils.livemarks.getSiteURI(this._itemId);
  915.       if (siteURI)
  916.         siteURISpec = siteURI.spec;
  917.       this._initTextField("siteLocationField", siteURISpec);
  918.       break;
  919.     }
  920.   },
  921.  
  922.   onItemMoved: function EIO_onItemMoved(aItemId, aOldParent, aOldIndex,
  923.                                         aNewParent, aNewIndex) {
  924.     if (aItemId != this._itemId ||
  925.         aNewParent == this._getFolderIdFromMenuList())
  926.       return;
  927.  
  928.     var folderItem = this._getFolderMenuItem(aNewParent);
  929.  
  930.     // just setting selectItem _does not_ trigger oncommand, so we don't
  931.     // recurse
  932.     this._folderMenuList.selectedItem = folderItem;
  933.   },
  934.  
  935.   onItemAdded: function EIO_onItemAdded(aItemId, aFolder, aIndex) {
  936.     this._lastNewItem = aItemId;
  937.   },
  938.  
  939.   onBeginUpdateBatch: function() { },
  940.   onEndUpdateBatch: function() { },
  941.   onItemRemoved: function() { },
  942.   onItemVisited: function() { },
  943. };
  944.