Wed, 31 Dec 2014 06:09:35 +0100
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 };