editBookmarkOverlay.js
Go to the documentation of this file.
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";
41 
43  _uri: null,
44  _itemId: -1,
45  _itemIds: [],
46  _uris: [],
47  _tags: [],
48  _allTags: [],
49  _multiEdit: false,
50  _itemType: -1,
51  _readOnly: false,
52  _microsummaries: null,
53  _hiddenRows: [],
54  _observersAdded: false,
55  _staticFoldersListBuilt: false,
56  _initialized: false,
57 
58  // the first field which was edited after this panel was initialized for
59  // a certain item
60  _firstEditedField: "",
61 
62  get itemId() {
63  return this._itemId;
64  },
65 
66  get uri() {
67  return this._uri;
68  },
69 
70  get multiEdit() {
71  return this._multiEdit;
72  },
73 
77  _determineInfo: function EIO__determineInfo(aInfo) {
78  // hidden rows
79  if (aInfo && aInfo.hiddenRows)
80  this._hiddenRows = aInfo.hiddenRows;
81  else
82  this._hiddenRows.splice(0);
83  // force-read-only
84  this._readOnly = aInfo && aInfo.forceReadOnly;
85  },
86 
87  _showHideRows: function EIO__showHideRows() {
88  var isBookmark = this._itemId != -1 &&
89  this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK;
90  var isQuery = false;
91  if (this._uri)
92  isQuery = this._uri.schemeIs("place");
93 
94  this._element("nameRow").collapsed = this._hiddenRows.indexOf("name") != -1;
95  this._element("folderRow").collapsed =
96  this._hiddenRows.indexOf("folderPicker") != -1 || this._readOnly;
97  this._element("tagsRow").collapsed = !this._uri ||
98  this._hiddenRows.indexOf("tags") != -1 || isQuery;
99  // Collapse the tag selector if the item does not accept tags.
100  if (!this._element("tagsSelectorRow").collapsed &&
101  this._element("tagsRow").collapsed)
102  this.toggleTagsSelector();
103  this._element("descriptionRow").collapsed =
104  this._hiddenRows.indexOf("description") != -1 || this._readOnly;
105  this._element("keywordRow").collapsed = !isBookmark || this._readOnly ||
106  this._hiddenRows.indexOf("keyword") != -1 || isQuery;
107  this._element("locationRow").collapsed = !(this._uri && !isQuery) ||
108  this._hiddenRows.indexOf("location") != -1;
109  this._element("loadInSidebarCheckbox").collapsed = !isBookmark || isQuery ||
110  this._readOnly || this._hiddenRows.indexOf("loadInSidebar") != -1;
111  this._element("feedLocationRow").collapsed = !this._isLivemark ||
112  this._hiddenRows.indexOf("feedLocation") != -1;
113  this._element("siteLocationRow").collapsed = !this._isLivemark ||
114  this._hiddenRows.indexOf("siteLocation") != -1;
115  this._element("selectionCount").hidden = !this._multiEdit;
116  },
117 
134  initPanel: function EIO_initPanel(aFor, aInfo) {
135  // For sanity ensure that the implementer has uninited the panel before
136  // trying to init it again, or we could end up leaking due to observers.
137  if (this._initialized)
138  this.uninitPanel(false);
139 
140  var aItemIdList;
141  if (aFor.length) {
142  aItemIdList = aFor;
143  aFor = aItemIdList[0];
144  }
145  else if (this._multiEdit) {
146  this._multiEdit = false;
147  this._tags = [];
148  this._uris = [];
149  this._allTags = [];
150  this._itemIds = [];
151  this._element("selectionCount").hidden = true;
152  }
153 
154  this._folderMenuList = this._element("folderMenuList");
155  this._folderTree = this._element("folderTree");
156 
157  this._determineInfo(aInfo);
158  if (aFor instanceof Ci.nsIURI) {
159  this._itemId = -1;
160  this._uri = aFor;
161  this._readOnly = true;
162  }
163  else {
164  this._itemId = aFor;
165  var containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
166  this._itemType = PlacesUtils.bookmarks.getItemType(this._itemId);
167  if (this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
168  this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
169  if (!this._readOnly) // If readOnly wasn't forced through aInfo
170  this._readOnly = PlacesUtils.itemIsLivemark(containerId);
171  this._initTextField("keywordField",
172  PlacesUtils.bookmarks
173  .getKeywordForBookmark(this._itemId));
174  // Load In Sidebar checkbox
175  this._element("loadInSidebarCheckbox").checked =
176  PlacesUtils.annotations.itemHasAnnotation(this._itemId,
178  }
179  else {
180  if (!this._readOnly) // If readOnly wasn't forced through aInfo
181  this._readOnly = false;
182 
183  this._uri = null;
184  this._isLivemark = PlacesUtils.itemIsLivemark(this._itemId);
185  if (this._isLivemark) {
186  var feedURI = PlacesUtils.livemarks.getFeedURI(this._itemId);
187  var siteURI = PlacesUtils.livemarks.getSiteURI(this._itemId);
188  this._initTextField("feedLocationField", feedURI.spec);
189  this._initTextField("siteLocationField", siteURI ? siteURI.spec : "");
190  }
191  }
192 
193  // folder picker
194  this._initFolderMenuList(containerId);
195 
196  // description field
197  this._initTextField("descriptionField",
198  PlacesUIUtils.getItemDescription(this._itemId));
199  }
200 
201  if (this._itemId == -1 ||
202  this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
203  this._isLivemark = false;
204 
205  this._initTextField("locationField", this._uri.spec);
206  if (!aItemIdList) {
207  var tags = PlacesUtils.tagging.getTagsForURI(this._uri, {}).join(", ");
208  this._initTextField("tagsField", tags, false);
209  }
210  else {
211  this._multiEdit = true;
212  this._allTags = [];
213  this._itemIds = aItemIdList;
214  var nodeToCheck = 0;
215  for (var i = 0; i < aItemIdList.length; i++) {
216  if (aItemIdList[i] instanceof Ci.nsIURI) {
217  this._uris[i] = aItemIdList[i];
218  this._itemIds[i] = -1;
219  }
220  else
221  this._uris[i] = PlacesUtils.bookmarks.getBookmarkURI(this._itemIds[i], {});
222  this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i], {});
223  if (this._tags[i].length < this._tags[nodeToCheck].length)
224  nodeToCheck = i;
225  }
226  this._getCommonTags(nodeToCheck);
227  this._initTextField("tagsField", this._allTags.join(", "), false);
228  this._element("itemsCountText").value =
229  PlacesUIUtils.getFormattedString("detailsPane.multipleItems",
230  [this._itemIds.length]);
231  }
232 
233  // tags selector
234  this._rebuildTagsSelectorList();
235  }
236 
237  // name picker
238  this._initNamePicker();
239 
240  this._showHideRows();
241 
242  // observe changes
243  if (!this._observersAdded) {
244  if (this._itemId != -1)
245  PlacesUtils.bookmarks.addObserver(this, false);
246  window.addEventListener("unload", this, false);
247  this._observersAdded = true;
248  }
249 
250  this._initialized = true;
251  },
252 
253  _getCommonTags: function(aArrIndex) {
254  var tempArray = this._tags[aArrIndex];
255  var isAllTag;
256  for (var k = 0; k < tempArray.length; k++) {
257  isAllTag = true;
258  for (var j = 0; j < this._tags.length; j++) {
259  if (j == aArrIndex)
260  continue;
261  if (this._tags[j].indexOf(tempArray[k]) == -1) {
262  isAllTag = false;
263  break;
264  }
265  }
266  if (isAllTag)
267  this._allTags.push(tempArray[k]);
268  }
269  },
270 
271  _initTextField: function(aTextFieldId, aValue, aReadOnly) {
272  var field = this._element(aTextFieldId);
273  field.readOnly = aReadOnly !== undefined ? aReadOnly : this._readOnly;
274 
275  if (field.value != aValue) {
276  field.value = aValue;
277 
278  // clear the undo stack
279  var editor = field.editor;
280  if (editor)
281  editor.transactionManager.clear();
282  }
283  },
284 
293  _appendFolderItemToMenupopup:
294  function EIO__appendFolderItemToMenuList(aMenupopup, aFolderId) {
295  // First make sure the folders-separator is visible
296  this._element("foldersSeparator").hidden = false;
297 
298  var folderMenuItem = document.createElement("menuitem");
299  var folderTitle = PlacesUtils.bookmarks.getItemTitle(aFolderId)
300  folderMenuItem.folderId = aFolderId;
301  folderMenuItem.setAttribute("label", folderTitle);
302  folderMenuItem.className = "menuitem-iconic folder-icon";
303  aMenupopup.appendChild(folderMenuItem);
304  return folderMenuItem;
305  },
306 
307  _initFolderMenuList: function EIO__initFolderMenuList(aSelectedFolder) {
308  // clean up first
309  var menupopup = this._folderMenuList.menupopup;
310  while (menupopup.childNodes.length > 6)
311  menupopup.removeChild(menupopup.lastChild);
312 
313  const bms = PlacesUtils.bookmarks;
314  const annos = PlacesUtils.annotations;
315 
316  // Build the static list
317  var unfiledItem = this._element("unfiledRootItem");
318  if (!this._staticFoldersListBuilt) {
319  unfiledItem.label = bms.getItemTitle(PlacesUtils.unfiledBookmarksFolderId);
320  unfiledItem.folderId = PlacesUtils.unfiledBookmarksFolderId;
321  var bmMenuItem = this._element("bmRootItem");
322  bmMenuItem.label = bms.getItemTitle(PlacesUtils.bookmarksMenuFolderId);
323  bmMenuItem.folderId = PlacesUtils.bookmarksMenuFolderId;
324  var toolbarItem = this._element("toolbarFolderItem");
325  toolbarItem.label = bms.getItemTitle(PlacesUtils.toolbarFolderId);
326  toolbarItem.folderId = PlacesUtils.toolbarFolderId;
327  this._staticFoldersListBuilt = true;
328  }
329 
330  // List of recently used folders:
331  var folderIds = annos.getItemsWithAnnotation(LAST_USED_ANNO, { });
332 
341  this._recentFolders = [];
342  for (var i = 0; i < folderIds.length; i++) {
343  var lastUsed = annos.getItemAnnotation(folderIds[i], LAST_USED_ANNO);
344  this._recentFolders.push({ folderId: folderIds[i], lastUsed: lastUsed });
345  }
346  this._recentFolders.sort(function(a, b) {
347  if (b.lastUsed < a.lastUsed)
348  return -1;
349  if (b.lastUsed > a.lastUsed)
350  return 1;
351  return 0;
352  });
353 
354  var numberOfItems = Math.min(MAX_FOLDER_ITEM_IN_MENU_LIST,
355  this._recentFolders.length);
356  for (var i = 0; i < numberOfItems; i++) {
357  this._appendFolderItemToMenupopup(menupopup,
358  this._recentFolders[i].folderId);
359  }
360 
361  var defaultItem = this._getFolderMenuItem(aSelectedFolder);
362  this._folderMenuList.selectedItem = defaultItem;
363 
364  // Set a selectedIndex attribute to show special icons
365  this._folderMenuList.setAttribute("selectedIndex",
366  this._folderMenuList.selectedIndex);
367 
368  // Hide the folders-separator if no folder is annotated as recently-used
369  this._element("foldersSeparator").hidden = (menupopup.childNodes.length <= 6);
370  this._folderMenuList.disabled = this._readOnly;
371  },
372 
373  QueryInterface: function EIO_QueryInterface(aIID) {
374  if (aIID.equals(Ci.nsIMicrosummaryObserver) ||
375  aIID.equals(Ci.nsIDOMEventListener) ||
376  aIID.equals(Ci.nsINavBookmarkObserver) ||
377  aIID.equals(Ci.nsISupports))
378  return this;
379 
380  throw Cr.NS_ERROR_NO_INTERFACE;
381  },
382 
383  _element: function EIO__element(aID) {
384  return document.getElementById("editBMPanel_" + aID);
385  },
386 
387  _createMicrosummaryMenuItem:
388  function EIO__createMicrosummaryMenuItem(aMicrosummary) {
389  var menuItem = document.createElement("menuitem");
390 
391  // Store a reference to the microsummary in the menu item, so we know
392  // which microsummary this menu item represents when it's time to
393  // save changes or load its content.
394  menuItem.microsummary = aMicrosummary;
395 
396  // Content may have to be generated asynchronously; we don't necessarily
397  // have it now. If we do, great; otherwise, fall back to the generator
398  // name, then the URI, and we trigger a microsummary content update. Once
399  // the update completes, the microsummary will notify our observer to
400  // update the corresponding menu-item.
401  // XXX Instead of just showing the generator name or (heaven forbid)
402  // its URI when we don't have content, we should tell the user that
403  // we're loading the microsummary, perhaps with some throbbing to let
404  // her know it is in progress.
405  if (aMicrosummary.content)
406  menuItem.setAttribute("label", aMicrosummary.content);
407  else {
408  menuItem.setAttribute("label", aMicrosummary.generator.name ||
409  aMicrosummary.generator.uri.spec);
410  aMicrosummary.update();
411  }
412 
413  return menuItem;
414  },
415 
416  _getItemStaticTitle: function EIO__getItemStaticTitle() {
417  if (this._itemId == -1)
418  return PlacesUtils.history.getPageTitle(this._uri);
419 
420  const annos = PlacesUtils.annotations;
421  if (annos.itemHasAnnotation(this._itemId, STATIC_TITLE_ANNO))
422  return annos.getItemAnnotation(this._itemId, STATIC_TITLE_ANNO);
423 
424  return PlacesUtils.bookmarks.getItemTitle(this._itemId);
425  },
426 
427  _initNamePicker: function EIO_initNamePicker() {
428  var userEnteredNameField = this._element("userEnteredName");
429  var namePicker = this._element("namePicker");
430  var droppable = false;
431 
432  userEnteredNameField.label = this._getItemStaticTitle();
433 
434  // clean up old entries
435  var menupopup = namePicker.menupopup;
436  while (menupopup.childNodes.length > 2)
437  menupopup.removeChild(menupopup.lastChild);
438 
439  if (this._microsummaries) {
440  this._microsummaries.removeObserver(this);
441  this._microsummaries = null;
442  }
443 
444  var itemToSelect = userEnteredNameField;
445  try {
446  if (this._itemId != -1 &&
447  this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK &&
448  !this._readOnly)
449  this._microsummaries = PlacesUIUtils.microsummaries
450  .getMicrosummaries(this._uri, -1);
451  }
452  catch(ex) {
453  // getMicrosummaries will throw an exception in at least two cases:
454  // 1. the bookmarked URI contains a scheme that the service won't
455  // download for security reasons (currently it only handles http,
456  // https, and file);
457  // 2. the page to which the URI refers isn't HTML or XML (the only two
458  // content types the service knows how to summarize).
459  this._microsummaries = null;
460  }
461  if (this._microsummaries) {
462  var enumerator = this._microsummaries.Enumerate();
463 
464  if (enumerator.hasMoreElements()) {
465  // Show the drop marker if there are microsummaries
466  droppable = true;
467  while (enumerator.hasMoreElements()) {
468  var microsummary = enumerator.getNext()
469  .QueryInterface(Ci.nsIMicrosummary);
470  var menuItem = this._createMicrosummaryMenuItem(microsummary);
471  if (PlacesUIUtils.microsummaries
472  .isMicrosummary(this._itemId, microsummary))
473  itemToSelect = menuItem;
474 
475  menupopup.appendChild(menuItem);
476  }
477  }
478 
479  this._microsummaries.addObserver(this);
480  }
481 
482  if (namePicker.selectedItem == itemToSelect)
483  namePicker.value = itemToSelect.label;
484  else
485  namePicker.selectedItem = itemToSelect;
486 
487  namePicker.setAttribute("droppable", droppable);
488  namePicker.readOnly = this._readOnly;
489 
490  // clear the undo stack
491  var editor = namePicker.editor;
492  if (editor)
493  editor.transactionManager.clear();
494  },
495 
496  // nsIMicrosummaryObserver
497  onContentLoaded: function EIO_onContentLoaded(aMicrosummary) {
498  var namePicker = this._element("namePicker");
499  var childNodes = namePicker.menupopup.childNodes;
500 
501  // 0: user-entered item; 1: separator
502  for (var i = 2; i < childNodes.length; i++) {
503  if (childNodes[i].microsummary == aMicrosummary) {
504  var newLabel = aMicrosummary.content;
505  // XXXmano: non-editable menulist would do this for us, see bug 360220
506  // We should fix editable-menulists to set the DOMAttrModified handler
507  // as well.
508  //
509  // Also note the order importance: if the label of the menu-item is
510  // set to something different than the menulist's current value,
511  // the menulist no longer has selectedItem set
512  if (namePicker.selectedItem == childNodes[i])
513  namePicker.value = newLabel;
514 
515  childNodes[i].label = newLabel;
516  return;
517  }
518  }
519  },
520 
521  onElementAppended: function EIO_onElementAppended(aMicrosummary) {
522  var namePicker = this._element("namePicker");
523  namePicker.menupopup
524  .appendChild(this._createMicrosummaryMenuItem(aMicrosummary));
525 
526  // Make sure the drop-marker is shown
527  namePicker.setAttribute("droppable", "true");
528  },
529 
530  uninitPanel: function EIO_uninitPanel(aHideCollapsibleElements) {
531  if (aHideCollapsibleElements) {
532  // hide the folder tree if it was previously visible
533  var folderTreeRow = this._element("folderTreeRow");
534  if (!folderTreeRow.collapsed)
535  this.toggleFolderTreeVisibility();
536 
537  // hide the tag selector if it was previously visible
538  var tagsSelectorRow = this._element("tagsSelectorRow");
539  if (!tagsSelectorRow.collapsed)
540  this.toggleTagsSelector();
541  }
542 
543  if (this._observersAdded) {
544  if (this._itemId != -1)
545  PlacesUtils.bookmarks.removeObserver(this);
546 
547  this._observersAdded = false;
548  }
549  if (this._microsummaries) {
550  this._microsummaries.removeObserver(this);
551  this._microsummaries = null;
552  }
553  this._itemId = -1;
554  this._uri = null;
555  this._uris = [];
556  this._tags = [];
557  this._allTags = [];
558  this._itemIds = [];
559  this._multiEdit = false;
560  this._firstEditedField = "";
561  this._initialized = false;
562  },
563 
564  onTagsFieldBlur: function EIO_onTagsFieldBlur() {
565  if (this._updateTags()) // if anything has changed
566  this._mayUpdateFirstEditField("tagsField");
567  },
568 
569  _updateTags: function EIO__updateTags() {
570  if (this._multiEdit)
571  return this._updateMultipleTagsForItems();
572  return this._updateSingleTagForItem();
573  },
574 
575  _updateSingleTagForItem: function EIO__updateSingleTagForItem() {
576  var currentTags = PlacesUtils.tagging.getTagsForURI(this._uri, { });
577  var tags = this._getTagsArrayFromTagField();
578  if (tags.length > 0 || currentTags.length > 0) {
579  var tagsToRemove = [];
580  var tagsToAdd = [];
581  var txns = [];
582  for (var i = 0; i < currentTags.length; i++) {
583  if (tags.indexOf(currentTags[i]) == -1)
584  tagsToRemove.push(currentTags[i]);
585  }
586  for (var i = 0; i < tags.length; i++) {
587  if (currentTags.indexOf(tags[i]) == -1)
588  tagsToAdd.push(tags[i]);
589  }
590 
591  if (tagsToRemove.length > 0)
592  txns.push(PlacesUIUtils.ptm.untagURI(this._uri, tagsToRemove));
593  if (tagsToAdd.length > 0)
594  txns.push(PlacesUIUtils.ptm.tagURI(this._uri, tagsToAdd));
595 
596  if (txns.length > 0) {
597  var aggregate = PlacesUIUtils.ptm.aggregateTransactions("Update tags",
598  txns);
599  PlacesUIUtils.ptm.doTransaction(aggregate);
600 
601  // Ensure the tagsField is in sync, clean it up from empty tags
602  var tags = PlacesUtils.tagging.getTagsForURI(this._uri, {}).join(", ");
603  this._initTextField("tagsField", tags, false);
604  return true;
605  }
606  }
607  return false;
608  },
609 
617  _mayUpdateFirstEditField: function EIO__mayUpdateFirstEditField(aNewField) {
618  // * The first-edit-field behavior is not applied in the multi-edit case
619  // * if this._firstEditedField is already set, this is not the first field,
620  // so there's nothing to do
621  if (this._multiEdit || this._firstEditedField)
622  return;
623 
624  this._firstEditedField = aNewField;
625 
626  // set the pref
627  var prefs = Cc["@mozilla.org/preferences-service;1"].
628  getService(Ci.nsIPrefBranch);
629  prefs.setCharPref("browser.bookmarks.editDialog.firstEditField", aNewField);
630  },
631 
632  _updateMultipleTagsForItems: function EIO__updateMultipleTagsForItems() {
633  var tags = this._getTagsArrayFromTagField();
634  if (tags.length > 0 || this._allTags.length > 0) {
635  var tagsToRemove = [];
636  var tagsToAdd = [];
637  var txns = [];
638  for (var i = 0; i < this._allTags.length; i++) {
639  if (tags.indexOf(this._allTags[i]) == -1)
640  tagsToRemove.push(this._allTags[i]);
641  }
642  for (var i = 0; i < this._tags.length; i++) {
643  tagsToAdd[i] = [];
644  for (var j = 0; j < tags.length; j++) {
645  if (this._tags[i].indexOf(tags[j]) == -1)
646  tagsToAdd[i].push(tags[j]);
647  }
648  }
649 
650  if (tagsToAdd.length > 0) {
651  for (i = 0; i < this._uris.length; i++) {
652  if (tagsToAdd[i].length > 0)
653  txns.push(PlacesUIUtils.ptm.tagURI(this._uris[i], tagsToAdd[i]));
654  }
655  }
656  if (tagsToRemove.length > 0) {
657  for (var i = 0; i < this._uris.length; i++)
658  txns.push(PlacesUIUtils.ptm.untagURI(this._uris[i], tagsToRemove));
659  }
660 
661  if (txns.length > 0) {
662  var aggregate = PlacesUIUtils.ptm.aggregateTransactions("Update tags",
663  txns);
664  PlacesUIUtils.ptm.doTransaction(aggregate);
665 
666  this._allTags = tags;
667  this._tags = [];
668  for (i = 0; i < this._uris.length; i++)
669  this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i], {});
670 
671  // Ensure the tagsField is in sync, clean it up from empty tags
672  this._initTextField("tagsField", tags, false);
673  return true;
674  }
675  }
676  return false;
677  },
678 
679  onNamePickerInput: function EIO_onNamePickerInput() {
680  var title = this._element("namePicker").value;
681  this._element("userEnteredName").label = title;
682  },
683 
684  onNamePickerChange: function EIO_onNamePickerChange() {
685  if (this._itemId == -1)
686  return;
687 
688  var namePicker = this._element("namePicker")
689  var txns = [];
690  const ptm = PlacesUIUtils.ptm;
691 
692  // Here we update either the item title or its cached static title
693  var newTitle = this._element("userEnteredName").label;
694  if (this._getItemStaticTitle() != newTitle) {
695  this._mayUpdateFirstEditField("namePicker");
696  if (PlacesUIUtils.microsummaries.hasMicrosummary(this._itemId)) {
697  // Note: this implicitly also takes care of the microsummary->static
698  // title case, the removeMicorosummary method in the service will set
699  // the item-title to the value of this annotation.
700  //
701  // XXXmano: use a transaction
702  PlacesUtils.setAnnotationsForItem(this._itemId,
704  value: newTitle}]);
705  }
706  else
707  txns.push(ptm.editItemTitle(this._itemId, newTitle));
708  }
709 
710  var newMicrosummary = namePicker.selectedItem.microsummary;
711 
712  // Only add a microsummary update to the transaction if the microsummary
713  // has actually changed, i.e. the user selected no microsummary, but the
714  // bookmark previously had one, or the user selected a microsummary which
715  // is not the one the bookmark previously had
716  if ((newMicrosummary == null &&
717  PlacesUIUtils.microsummaries.hasMicrosummary(this._itemId)) ||
718  (newMicrosummary != null &&
719  !PlacesUIUtils.microsummaries
720  .isMicrosummary(this._itemId, newMicrosummary))) {
721  txns.push(ptm.editBookmarkMicrosummary(this._itemId, newMicrosummary));
722  }
723 
724  var aggregate = ptm.aggregateTransactions("Edit Item Title", txns);
725  ptm.doTransaction(aggregate);
726  },
727 
728  onDescriptionFieldBlur: function EIO_onDescriptionFieldBlur() {
729  var description = this._element("descriptionField").value;
730  if (description != PlacesUIUtils.getItemDescription(this._itemId)) {
731  var txn = PlacesUIUtils.ptm
732  .editItemDescription(this._itemId, description);
733  PlacesUIUtils.ptm.doTransaction(txn);
734  }
735  },
736 
737  onLocationFieldBlur: function EIO_onLocationFieldBlur() {
738  var uri;
739  try {
740  uri = PlacesUIUtils.createFixedURI(this._element("locationField").value);
741  }
742  catch(ex) { return; }
743 
744  if (!this._uri.equals(uri)) {
745  var txn = PlacesUIUtils.ptm.editBookmarkURI(this._itemId, uri);
746  PlacesUIUtils.ptm.doTransaction(txn);
747  this._uri = uri;
748  }
749  },
750 
751  onKeywordFieldBlur: function EIO_onKeywordFieldBlur() {
752  var keyword = this._element("keywordField").value;
753  if (keyword != PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId)) {
754  var txn = PlacesUIUtils.ptm.editBookmarkKeyword(this._itemId, keyword);
755  PlacesUIUtils.ptm.doTransaction(txn);
756  }
757  },
758 
759  onFeedLocationFieldBlur: function EIO_onFeedLocationFieldBlur() {
760  var uri;
761  try {
762  uri = PlacesUIUtils.createFixedURI(this._element("feedLocationField").value);
763  }
764  catch(ex) { return; }
765 
766  var currentFeedURI = PlacesUtils.livemarks.getFeedURI(this._itemId);
767  if (!currentFeedURI.equals(uri)) {
768  var txn = PlacesUIUtils.ptm.editLivemarkFeedURI(this._itemId, uri);
769  PlacesUIUtils.ptm.doTransaction(txn);
770  }
771  },
772 
773  onSiteLocationFieldBlur: function EIO_onSiteLocationFieldBlur() {
774  var uri = null;
775  try {
776  uri = PlacesUIUtils.createFixedURI(this._element("siteLocationField").value);
777  }
778  catch(ex) { }
779 
780  var currentSiteURI = PlacesUtils.livemarks.getSiteURI(this._itemId);
781  if (!uri || !currentSiteURI.equals(uri)) {
782  var txn = PlacesUIUtils.ptm.editLivemarkSiteURI(this._itemId, uri);
783  PlacesUIUtils.ptm.doTransaction(txn);
784  }
785  },
786 
787  onLoadInSidebarCheckboxCommand:
788  function EIO_onLoadInSidebarCheckboxCommand() {
789  var loadInSidebarChecked = this._element("loadInSidebarCheckbox").checked;
790  var txn = PlacesUIUtils.ptm.setLoadInSidebar(this._itemId,
791  loadInSidebarChecked);
792  PlacesUIUtils.ptm.doTransaction(txn);
793  },
794 
795  toggleFolderTreeVisibility: function EIO_toggleFolderTreeVisibility() {
796  var expander = this._element("foldersExpander");
797  var folderTreeRow = this._element("folderTreeRow");
798  if (!folderTreeRow.collapsed) {
799  expander.className = "expander-down";
800  expander.setAttribute("tooltiptext",
801  expander.getAttribute("tooltiptextdown"));
802  folderTreeRow.collapsed = true;
803  this._element("chooseFolderSeparator").hidden =
804  this._element("chooseFolderMenuItem").hidden = false;
805  }
806  else {
807  expander.className = "expander-up"
808  expander.setAttribute("tooltiptext",
809  expander.getAttribute("tooltiptextup"));
810  folderTreeRow.collapsed = false;
811 
812  // XXXmano: Ideally we would only do this once, but for some odd reason,
813  // the editable mode set on this tree, together with its collapsed state
814  // breaks the view.
815  const FOLDER_TREE_PLACE_URI =
816  "place:excludeItems=1&excludeQueries=1&excludeReadOnlyFolders=1&folder=" +
817  PlacesUIUtils.allBookmarksFolderId;
818  this._folderTree.place = FOLDER_TREE_PLACE_URI;
819 
820  this._element("chooseFolderSeparator").hidden =
821  this._element("chooseFolderMenuItem").hidden = true;
822  var currentFolder = this._getFolderIdFromMenuList();
823  this._folderTree.selectItems([currentFolder]);
824  this._folderTree.focus();
825  }
826  },
827 
828  _getFolderIdFromMenuList:
829  function EIO__getFolderIdFromMenuList() {
830  var selectedItem = this._folderMenuList.selectedItem;
831  NS_ASSERT("folderId" in selectedItem,
832  "Invalid menuitem in the folders-menulist");
833  return selectedItem.folderId;
834  },
835 
844  _getFolderMenuItem:
845  function EIO__getFolderMenuItem(aFolderId) {
846  var menupopup = this._folderMenuList.menupopup;
847 
848  for (var i=0; i < menupopup.childNodes.length; i++) {
849  if (menupopup.childNodes[i].folderId &&
850  menupopup.childNodes[i].folderId == aFolderId)
851  return menupopup.childNodes[i];
852  }
853 
854  // 3 special folders + separator + folder-items-count limit
855  if (menupopup.childNodes.length == 4 + MAX_FOLDER_ITEM_IN_MENU_LIST)
856  menupopup.removeChild(menupopup.lastChild);
857 
858  return this._appendFolderItemToMenupopup(menupopup, aFolderId);
859  },
860 
861  onFolderMenuListCommand: function EIO_onFolderMenuListCommand(aEvent) {
862  // Set a selectedIndex attribute to show special icons
863  this._folderMenuList.setAttribute("selectedIndex",
864  this._folderMenuList.selectedIndex);
865 
866  if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") {
867  // reset the selection back to where it was and expand the tree
868  // (this menu-item is hidden when the tree is already visible
869  var container = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
870  var item = this._getFolderMenuItem(container);
871  this._folderMenuList.selectedItem = item;
872  // XXXmano HACK: setTimeout 100, otherwise focus goes back to the
873  // menulist right away
874  setTimeout(function(self) self.toggleFolderTreeVisibility(), 100, this);
875  return;
876  }
877 
878  // Move the item
879  var container = this._getFolderIdFromMenuList();
880  if (PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) != container) {
881  var txn = PlacesUIUtils.ptm.moveItem(this._itemId, container, -1);
882  PlacesUIUtils.ptm.doTransaction(txn);
883 
884  // Mark the containing folder as recently-used if it isn't in the
885  // static list
886  if (container != PlacesUtils.unfiledBookmarksFolderId &&
887  container != PlacesUtils.toolbarFolderId &&
888  container != PlacesUtils.bookmarksMenuFolderId)
889  this._markFolderAsRecentlyUsed(container);
890  }
891 
892  // Update folder-tree selection
893  var folderTreeRow = this._element("folderTreeRow");
894  if (!folderTreeRow.collapsed) {
895  var selectedNode = this._folderTree.selectedNode;
896  if (!selectedNode ||
897  PlacesUtils.getConcreteItemId(selectedNode) != container)
898  this._folderTree.selectItems([container]);
899  }
900  },
901 
902  onFolderTreeSelect: function EIO_onFolderTreeSelect() {
903  var selectedNode = this._folderTree.selectedNode;
904 
905  // Disable the "New Folder" button if we cannot create a new folder
906  this._element("newFolderButton")
907  .disabled = !this._folderTree.insertionPoint || !selectedNode;
908 
909  if (!selectedNode)
910  return;
911 
912  var folderId = PlacesUtils.getConcreteItemId(selectedNode);
913  if (this._getFolderIdFromMenuList() == folderId)
914  return;
915 
916  var folderItem = this._getFolderMenuItem(folderId);
917  this._folderMenuList.selectedItem = folderItem;
918  folderItem.doCommand();
919  },
920 
921  _markFolderAsRecentlyUsed:
922  function EIO__markFolderAsRecentlyUsed(aFolderId) {
923  var txns = [];
924 
925  // Expire old unused recent folders
926  var anno = this._getLastUsedAnnotationObject(false);
927  while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) {
928  var folderId = this._recentFolders.pop().folderId;
929  txns.push(PlacesUIUtils.ptm.setItemAnnotation(folderId, anno));
930  }
931 
932  // Mark folder as recently used
933  anno = this._getLastUsedAnnotationObject(true);
934  txns.push(PlacesUIUtils.ptm.setItemAnnotation(aFolderId, anno));
935 
936  var aggregate = PlacesUIUtils.ptm.aggregateTransactions("Update last used folders", txns);
937  PlacesUIUtils.ptm.doTransaction(aggregate);
938  },
939 
949  _getLastUsedAnnotationObject:
950  function EIO__getLastUsedAnnotationObject(aLastUsed) {
951  var anno = { name: LAST_USED_ANNO,
952  type: Ci.nsIAnnotationService.TYPE_INT32,
953  flags: 0,
954  value: aLastUsed ? new Date().getTime() : null,
955  expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
956 
957  return anno;
958  },
959 
960  _rebuildTagsSelectorList: function EIO__rebuildTagsSelectorList() {
961  var tagsSelector = this._element("tagsSelector");
962  var tagsSelectorRow = this._element("tagsSelectorRow");
963  if (tagsSelectorRow.collapsed)
964  return;
965 
966  while (tagsSelector.hasChildNodes())
967  tagsSelector.removeChild(tagsSelector.lastChild);
968 
969  var tagsInField = this._getTagsArrayFromTagField();
970  var allTags = PlacesUtils.tagging.allTags;
971  for (var i = 0; i < allTags.length; i++) {
972  var tag = allTags[i];
973  var elt = document.createElement("listitem");
974  elt.setAttribute("type", "checkbox");
975  elt.setAttribute("label", tag);
976  if (tagsInField.indexOf(tag) != -1)
977  elt.setAttribute("checked", "true");
978 
979  tagsSelector.appendChild(elt);
980  }
981  },
982 
983  toggleTagsSelector: function EIO_toggleTagsSelector() {
984  var tagsSelector = this._element("tagsSelector");
985  var tagsSelectorRow = this._element("tagsSelectorRow");
986  var expander = this._element("tagsSelectorExpander");
987  if (tagsSelectorRow.collapsed) {
988  expander.className = "expander-up";
989  expander.setAttribute("tooltiptext",
990  expander.getAttribute("tooltiptextup"));
991  tagsSelectorRow.collapsed = false;
992  this._rebuildTagsSelectorList();
993 
994  // This is a no-op if we've added the listener.
995  tagsSelector.addEventListener("CheckboxStateChange", this, false);
996  }
997  else {
998  expander.className = "expander-down";
999  expander.setAttribute("tooltiptext",
1000  expander.getAttribute("tooltiptextdown"));
1001  tagsSelectorRow.collapsed = true;
1002  }
1003  },
1004 
1005  _getTagsArrayFromTagField: function EIO__getTagsArrayFromTagField() {
1006  // we don't require the leading space (after each comma)
1007  var tags = this._element("tagsField").value.split(",");
1008  for (var i=0; i < tags.length; i++) {
1009  // remove trailing and leading spaces
1010  tags[i] = tags[i].replace(/^\s+/, "").replace(/\s+$/, "");
1011 
1012  // remove empty entries from the array.
1013  if (tags[i] == "") {
1014  tags.splice(i, 1);
1015  i--;
1016  }
1017  }
1018  return tags;
1019  },
1020 
1021  newFolder: function EIO_newFolder() {
1022  var ip = this._folderTree.insertionPoint;
1023 
1024  // default to the bookmarks menu folder
1025  if (!ip || ip.itemId == PlacesUIUtils.allBookmarksFolderId) {
1026  ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
1027  PlacesUtils.bookmarks.DEFAULT_INDEX,
1028  Ci.nsITreeView.DROP_ON);
1029  }
1030 
1031  // XXXmano: add a separate "New Folder" string at some point...
1032  var defaultLabel = this._element("newFolderButton").label;
1033  var txn = PlacesUIUtils.ptm.createFolder(defaultLabel, ip.itemId, ip.index);
1034  PlacesUIUtils.ptm.doTransaction(txn);
1035  this._folderTree.focus();
1036  this._folderTree.selectItems([this._lastNewItem]);
1037  this._folderTree.startEditing(this._folderTree.view.selection.currentIndex,
1038  this._folderTree.columns.getFirstColumn());
1039  },
1040 
1041  // nsIDOMEventListener
1042  handleEvent: function EIO_nsIDOMEventListener(aEvent) {
1043  switch (aEvent.type) {
1044  case "CheckboxStateChange":
1045  // Update the tags field when items are checked/unchecked in the listbox
1046  var tags = this._getTagsArrayFromTagField();
1047 
1048  if (aEvent.target.checked) {
1049  if (tags.indexOf(aEvent.target.label) == -1)
1050  tags.push(aEvent.target.label);
1051  }
1052  else {
1053  var indexOfItem = tags.indexOf(aEvent.target.label);
1054  if (indexOfItem != -1)
1055  tags.splice(indexOfItem, 1);
1056  }
1057  this._element("tagsField").value = tags.join(", ");
1058  this._updateTags();
1059  break;
1060  case "unload":
1061  this.uninitPanel(false);
1062  break;
1063  }
1064  },
1065 
1066  // nsINavBookmarkObserver
1067  onItemChanged: function EIO_onItemChanged(aItemId, aProperty,
1068  aIsAnnotationProperty, aValue,
1069  aLastModified, aItemType) {
1070  if (this._itemId != aItemId) {
1071  if (aProperty == "title") {
1072  // If the title of a folder which is listed within the folders
1073  // menulist has been changed, we need to update the label of its
1074  // representing element.
1075  var menupopup = this._folderMenuList.menupopup;
1076  for (var i=0; i < menupopup.childNodes.length; i++) {
1077  if (menupopup.childNodes[i].folderId == aItemId) {
1078  menupopup.childNodes[i].label = aValue;
1079  break;
1080  }
1081  }
1082  }
1083 
1084  return;
1085  }
1086 
1087  switch (aProperty) {
1088  case "title":
1089  if (PlacesUtils.annotations.itemHasAnnotation(this._itemId,
1091  return; // onContentLoaded updates microsummary-items
1092 
1093  var userEnteredNameField = this._element("userEnteredName");
1094  if (userEnteredNameField.value != aValue) {
1095  userEnteredNameField.value = aValue;
1096  var namePicker = this._element("namePicker");
1097  if (namePicker.selectedItem == userEnteredNameField) {
1098  namePicker.label = aValue;
1099 
1100  // clear undo stack
1101  namePicker.editor.transactionManager.clear();
1102  }
1103  }
1104  break;
1105  case "uri":
1106  var locationField = this._element("locationField");
1107  if (locationField.value != aValue) {
1108  this._uri = Cc["@mozilla.org/network/io-service;1"].
1109  getService(Ci.nsIIOService).
1110  newURI(aValue, null, null);
1111  this._initTextField("locationField", this._uri.spec);
1112  this._initNamePicker(); // for microsummaries
1113  this._initTextField("tagsField",
1114  PlacesUtils.tagging
1115  .getTagsForURI(this._uri, { }).join(", "),
1116  false);
1117  this._rebuildTagsSelectorList();
1118  }
1119  break;
1120  case "keyword":
1121  this._initTextField("keywordField",
1122  PlacesUtils.bookmarks
1123  .getKeywordForBookmark(this._itemId));
1124  break;
1125  case DESCRIPTION_ANNO:
1126  this._initTextField("descriptionField",
1127  PlacesUIUtils.getItemDescription(this._itemId));
1128  break;
1129  case LOAD_IN_SIDEBAR_ANNO:
1130  this._element("loadInSidebarCheckbox").checked =
1131  PlacesUtils.annotations.itemHasAnnotation(this._itemId,
1133  break;
1134  case LMANNO_FEEDURI:
1135  var feedURISpec = PlacesUtils.livemarks.getFeedURI(this._itemId).spec;
1136  this._initTextField("feedLocationField", feedURISpec);
1137  break;
1138  case LMANNO_SITEURI:
1139  var siteURISpec = "";
1140  var siteURI = PlacesUtils.livemarks.getSiteURI(this._itemId);
1141  if (siteURI)
1142  siteURISpec = siteURI.spec;
1143  this._initTextField("siteLocationField", siteURISpec);
1144  break;
1145  }
1146  },
1147 
1148  onItemMoved: function EIO_onItemMoved(aItemId, aOldParent, aOldIndex,
1149  aNewParent, aNewIndex, aItemType) {
1150  if (aItemId != this._itemId ||
1151  aNewParent == this._getFolderIdFromMenuList())
1152  return;
1153 
1154  var folderItem = this._getFolderMenuItem(aNewParent);
1155 
1156  // just setting selectItem _does not_ trigger oncommand, so we don't
1157  // recurse
1158  this._folderMenuList.selectedItem = folderItem;
1159  },
1160 
1161  onItemAdded: function EIO_onItemAdded(aItemId, aFolder, aIndex, aItemType) {
1162  this._lastNewItem = aItemId;
1163  },
1164 
1165  onBeginUpdateBatch: function() { },
1166  onEndUpdateBatch: function() { },
1167  onBeforeItemRemoved: function() { },
1168  onItemRemoved: function() { },
1169  onItemVisited: function() { },
1170 };
var PlacesUIUtils
Definition: utils.js:85
const LMANNO_SITEURI
Definition: utils.js:62
const Cc
var gEditItemOverlay
#define DESCRIPTION_ANNO
var menuItem
Definition: FeedWriter.js:970
onPageChanged aValue
Definition: FeedWriter.js:1395
const MAX_FOLDER_ITEM_IN_MENU_LIST
const LAST_USED_ANNO
onPageChanged onEndUpdateBatch
Definition: FeedWriter.js:1395
sbDeviceFirmwareAutoCheckForUpdate prototype flags
const LMANNO_FEEDURI
Definition: utils.js:61
sbOSDControlService prototype QueryInterface
getService(Ci.sbIFaceplateManager)
return elem filter &&elem filter indexOf("opacity=")>=0?(parseFloat(elem.filter.match(/opacity
let window
this _contentSandbox label
Definition: FeedWriter.js:814
var selectedItem
Definition: FeedWriter.js:1261
aWindow setTimeout(function(){_this.restoreHistory(aWindow, aTabs, aTabData, aIdMap);}, 0)
this _document this
Definition: FeedWriter.js:1085
onPageChanged onBeginUpdateBatch
Definition: FeedWriter.js:1395
return null
Definition: FeedWriter.js:1143
function newURI(aURLString)
function NS_ASSERT(cond, msg)
Definition: httpd.js:70
var uri
Definition: FeedWriter.js:1135
var prefs
Definition: FeedWriter.js:1169
countRef value
Definition: FeedWriter.js:1423
const Cr
const STATIC_TITLE_ANNO
if(DEBUG_DATAREMOTES)
const Ci
#define LOAD_IN_SIDEBAR_ANNO
dataSBGenres SBProperties tag
Definition: tuner2.js:871
function InsertionPoint(aItemId, aIndex, aOrientation, aIsTag, aDropNearItemId)
Definition: controller.js:84
_getSelectedPageStyle s i