|
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/. */ |
|
4 |
|
5 const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed"; |
|
6 const MAX_FOLDER_ITEM_IN_MENU_LIST = 5; |
|
7 |
|
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: "", |
|
23 |
|
24 // the first field which was edited after this panel was initialized for |
|
25 // a certain item |
|
26 _firstEditedField: "", |
|
27 |
|
28 get itemId() { |
|
29 return this._itemId; |
|
30 }, |
|
31 |
|
32 get uri() { |
|
33 return this._uri; |
|
34 }, |
|
35 |
|
36 get multiEdit() { |
|
37 return this._multiEdit; |
|
38 }, |
|
39 |
|
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 }, |
|
54 |
|
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"); |
|
61 |
|
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 }, |
|
85 |
|
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); |
|
107 |
|
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 } |
|
121 |
|
122 this._folderMenuList = this._element("folderMenuList"); |
|
123 this._folderTree = this._element("folderTree"); |
|
124 |
|
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; |
|
135 |
|
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 } |
|
158 |
|
159 // folder picker |
|
160 this._initFolderMenuList(containerId); |
|
161 |
|
162 // description field |
|
163 this._initTextField("descriptionField", |
|
164 PlacesUIUtils.getItemDescription(this._itemId)); |
|
165 } |
|
166 |
|
167 if (this._itemId == -1 || |
|
168 this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) { |
|
169 this._isLivemark = false; |
|
170 |
|
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 } |
|
196 |
|
197 // tags selector |
|
198 this._rebuildTagsSelectorList(); |
|
199 } |
|
200 |
|
201 // name picker |
|
202 this._initNamePicker(); |
|
203 |
|
204 this._showHideRows(); |
|
205 |
|
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 } |
|
215 |
|
216 this._initialized = true; |
|
217 }, |
|
218 |
|
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 }, |
|
233 |
|
234 _initTextField: function(aTextFieldId, aValue, aReadOnly) { |
|
235 var field = this._element(aTextFieldId); |
|
236 field.readOnly = aReadOnly !== undefined ? aReadOnly : this._readOnly; |
|
237 |
|
238 if (field.value != aValue) { |
|
239 field.value = aValue; |
|
240 |
|
241 // clear the undo stack |
|
242 var editor = field.editor; |
|
243 if (editor) |
|
244 editor.transactionManager.clear(); |
|
245 } |
|
246 }, |
|
247 |
|
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; |
|
260 |
|
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 }, |
|
269 |
|
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); |
|
275 |
|
276 const bms = PlacesUtils.bookmarks; |
|
277 const annos = PlacesUtils.annotations; |
|
278 |
|
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 } |
|
292 |
|
293 // List of recently used folders: |
|
294 var folderIds = annos.getItemsWithAnnotation(LAST_USED_ANNO); |
|
295 |
|
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 }); |
|
316 |
|
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 } |
|
323 |
|
324 var defaultItem = this._getFolderMenuItem(aSelectedFolder); |
|
325 this._folderMenuList.selectedItem = defaultItem; |
|
326 |
|
327 // Set a selectedIndex attribute to show special icons |
|
328 this._folderMenuList.setAttribute("selectedIndex", |
|
329 this._folderMenuList.selectedIndex); |
|
330 |
|
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 }, |
|
335 |
|
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; |
|
341 |
|
342 throw Cr.NS_ERROR_NO_INTERFACE; |
|
343 }, |
|
344 |
|
345 _element: function EIO__element(aID) { |
|
346 return document.getElementById("editBMPanel_" + aID); |
|
347 }, |
|
348 |
|
349 _getItemStaticTitle: function EIO__getItemStaticTitle() { |
|
350 if (this._titleOverride) |
|
351 return this._titleOverride; |
|
352 |
|
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 }, |
|
362 |
|
363 _initNamePicker: function EIO_initNamePicker() { |
|
364 var namePicker = this._element("namePicker"); |
|
365 namePicker.value = this._getItemStaticTitle(); |
|
366 namePicker.readOnly = this._readOnly; |
|
367 |
|
368 // clear the undo stack |
|
369 var editor = namePicker.editor; |
|
370 if (editor) |
|
371 editor.transactionManager.clear(); |
|
372 }, |
|
373 |
|
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(); |
|
380 |
|
381 // hide the tag selector if it was previously visible |
|
382 var tagsSelectorRow = this._element("tagsSelectorRow"); |
|
383 if (!tagsSelectorRow.collapsed) |
|
384 this.toggleTagsSelector(); |
|
385 } |
|
386 |
|
387 if (this._observersAdded) { |
|
388 if (this._itemId != -1 || this._uri || this._multiEdit) |
|
389 PlacesUtils.bookmarks.removeObserver(this); |
|
390 |
|
391 this._observersAdded = false; |
|
392 } |
|
393 |
|
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 }, |
|
406 |
|
407 onTagsFieldBlur: function EIO_onTagsFieldBlur() { |
|
408 if (this._updateTags()) // if anything has changed |
|
409 this._mayUpdateFirstEditField("tagsField"); |
|
410 }, |
|
411 |
|
412 _updateTags: function EIO__updateTags() { |
|
413 if (this._multiEdit) |
|
414 return this._updateMultipleTagsForItems(); |
|
415 return this._updateSingleTagForItem(); |
|
416 }, |
|
417 |
|
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 } |
|
433 |
|
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 } |
|
442 |
|
443 if (txns.length > 0) { |
|
444 let aggregate = new PlacesAggregatedTransaction("Update tags", txns); |
|
445 PlacesUtils.transactionManager.doTransaction(aggregate); |
|
446 |
|
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 }, |
|
455 |
|
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; |
|
469 |
|
470 this._firstEditedField = aNewField; |
|
471 |
|
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 }, |
|
477 |
|
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 } |
|
495 |
|
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 } |
|
512 |
|
513 if (txns.length > 0) { |
|
514 let aggregate = new PlacesAggregatedTransaction("Update tags", txns); |
|
515 PlacesUtils.transactionManager.doTransaction(aggregate); |
|
516 |
|
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 } |
|
522 |
|
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 }, |
|
530 |
|
531 onNamePickerChange: function EIO_onNamePickerChange() { |
|
532 if (this._itemId == -1) |
|
533 return; |
|
534 |
|
535 var namePicker = this._element("namePicker") |
|
536 |
|
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 }, |
|
550 |
|
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 }, |
|
563 |
|
564 onLocationFieldBlur: function EIO_onLocationFieldBlur() { |
|
565 var uri; |
|
566 try { |
|
567 uri = PlacesUIUtils.createFixedURI(this._element("locationField").value); |
|
568 } |
|
569 catch(ex) { return; } |
|
570 |
|
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 }, |
|
577 |
|
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 }, |
|
585 |
|
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 }, |
|
594 |
|
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; |
|
611 |
|
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; |
|
619 |
|
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 }, |
|
627 |
|
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 }, |
|
635 |
|
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; |
|
647 |
|
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 } |
|
653 |
|
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); |
|
657 |
|
658 return this._appendFolderItemToMenupopup(menupopup, aFolderId); |
|
659 }, |
|
660 |
|
661 onFolderMenuListCommand: function EIO_onFolderMenuListCommand(aEvent) { |
|
662 // Set a selectedIndex attribute to show special icons |
|
663 this._folderMenuList.setAttribute("selectedIndex", |
|
664 this._folderMenuList.selectedIndex); |
|
665 |
|
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 } |
|
677 |
|
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); |
|
685 |
|
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 } |
|
693 |
|
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 }, |
|
703 |
|
704 onFolderTreeSelect: function EIO_onFolderTreeSelect() { |
|
705 var selectedNode = this._folderTree.selectedNode; |
|
706 |
|
707 // Disable the "New Folder" button if we cannot create a new folder |
|
708 this._element("newFolderButton") |
|
709 .disabled = !this._folderTree.insertionPoint || !selectedNode; |
|
710 |
|
711 if (!selectedNode) |
|
712 return; |
|
713 |
|
714 var folderId = PlacesUtils.getConcreteItemId(selectedNode); |
|
715 if (this._getFolderIdFromMenuList() == folderId) |
|
716 return; |
|
717 |
|
718 var folderItem = this._getFolderMenuItem(folderId); |
|
719 this._folderMenuList.selectedItem = folderItem; |
|
720 folderItem.doCommand(); |
|
721 }, |
|
722 |
|
723 _markFolderAsRecentlyUsed: |
|
724 function EIO__markFolderAsRecentlyUsed(aFolderId) { |
|
725 var txns = []; |
|
726 |
|
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 } |
|
734 |
|
735 // Mark folder as recently used |
|
736 anno = this._getLastUsedAnnotationObject(true); |
|
737 let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno); |
|
738 txns.push(annoTxn); |
|
739 |
|
740 let aggregate = new PlacesAggregatedTransaction("Update last used folders", txns); |
|
741 PlacesUtils.transactionManager.doTransaction(aggregate); |
|
742 }, |
|
743 |
|
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 }; |
|
760 |
|
761 return anno; |
|
762 }, |
|
763 |
|
764 _rebuildTagsSelectorList: function EIO__rebuildTagsSelectorList() { |
|
765 var tagsSelector = this._element("tagsSelector"); |
|
766 var tagsSelectorRow = this._element("tagsSelectorRow"); |
|
767 if (tagsSelectorRow.collapsed) |
|
768 return; |
|
769 |
|
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; |
|
775 |
|
776 while (tagsSelector.hasChildNodes()) |
|
777 tagsSelector.removeChild(tagsSelector.lastChild); |
|
778 |
|
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 } |
|
792 |
|
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 }, |
|
806 |
|
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(); |
|
817 |
|
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 }, |
|
828 |
|
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 }, |
|
840 |
|
841 newFolder: function EIO_newFolder() { |
|
842 var ip = this._folderTree.insertionPoint; |
|
843 |
|
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 } |
|
850 |
|
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 }, |
|
862 |
|
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(); |
|
869 |
|
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 }, |
|
887 |
|
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 } |
|
911 |
|
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 } |
|
922 |
|
923 // Any tags change should be reflected in the tags selector. |
|
924 this._rebuildTagsSelectorList(); |
|
925 return; |
|
926 } |
|
927 |
|
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 } |
|
942 |
|
943 return; |
|
944 } |
|
945 |
|
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 }, |
|
1001 |
|
1002 onItemMoved: function EIO_onItemMoved(aItemId, aOldParent, aOldIndex, |
|
1003 aNewParent, aNewIndex, aItemType) { |
|
1004 if (aItemId != this._itemId || |
|
1005 aNewParent == this._getFolderIdFromMenuList()) |
|
1006 return; |
|
1007 |
|
1008 var folderItem = this._getFolderMenuItem(aNewParent); |
|
1009 |
|
1010 // just setting selectItem _does not_ trigger oncommand, so we don't |
|
1011 // recurse |
|
1012 this._folderMenuList.selectedItem = folderItem; |
|
1013 }, |
|
1014 |
|
1015 onItemAdded: function EIO_onItemAdded(aItemId, aParentId, aIndex, aItemType, |
|
1016 aURI) { |
|
1017 this._lastNewItem = aItemId; |
|
1018 }, |
|
1019 |
|
1020 onItemRemoved: function() { }, |
|
1021 onBeginUpdateBatch: function() { }, |
|
1022 onEndUpdateBatch: function() { }, |
|
1023 onItemVisited: function() { }, |
|
1024 }; |