editBookmarkOverlay.js 38 KB

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