places.js 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533
  1. /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  6. XPCOMUtils.defineLazyModuleGetter(this, "Task",
  7. "resource://gre/modules/Task.jsm");
  8. XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
  9. "resource://gre/modules/BookmarkJSONUtils.jsm");
  10. XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
  11. "resource://gre/modules/PlacesBackups.jsm");
  12. const RESTORE_FILEPICKER_FILTER_EXT = "*.json;*.jsonlz4";
  13. var PlacesOrganizer = {
  14. _places: null,
  15. // IDs of fields from editBookmarkOverlay that should be hidden when infoBox
  16. // is minimal. IDs should be kept in sync with the IDs of the elements
  17. // observing additionalInfoBroadcaster.
  18. _additionalInfoFields: [
  19. "editBMPanel_descriptionRow",
  20. "editBMPanel_loadInSidebarCheckbox",
  21. "editBMPanel_keywordRow",
  22. ],
  23. _initFolderTree: function() {
  24. var leftPaneRoot = PlacesUIUtils.leftPaneFolderId;
  25. this._places.place = "place:excludeItems=1&expandQueries=0&folder=" + leftPaneRoot;
  26. },
  27. selectLeftPaneQuery: function(aQueryName) {
  28. var itemId = PlacesUIUtils.leftPaneQueries[aQueryName];
  29. this._places.selectItems([itemId]);
  30. // Forcefully expand all-bookmarks
  31. if (aQueryName == "AllBookmarks" || aQueryName == "History")
  32. PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
  33. },
  34. init: function() {
  35. ContentArea.init();
  36. this._places = document.getElementById("placesList");
  37. this._initFolderTree();
  38. var leftPaneSelection = "AllBookmarks"; // default to all-bookmarks
  39. if (window.arguments && window.arguments[0])
  40. leftPaneSelection = window.arguments[0];
  41. this.selectLeftPaneQuery(leftPaneSelection);
  42. if (leftPaneSelection == "History") {
  43. let historyNode = this._places.selectedNode;
  44. if (historyNode.childCount > 0)
  45. this._places.selectNode(historyNode.getChild(0));
  46. }
  47. // clear the back-stack
  48. this._backHistory.splice(0, this._backHistory.length);
  49. document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
  50. // Set up the search UI.
  51. PlacesSearchBox.init();
  52. window.addEventListener("AppCommand", this, true);
  53. // remove the "Properties" context-menu item, we've our own details pane
  54. document.getElementById("placesContext")
  55. .removeChild(document.getElementById("placesContext_show:info"));
  56. ContentArea.focus();
  57. },
  58. QueryInterface: function(aIID) {
  59. if (aIID.equals(Components.interfaces.nsIDOMEventListener) ||
  60. aIID.equals(Components.interfaces.nsISupports))
  61. return this;
  62. throw new Components.Exception("", Components.results.NS_NOINTERFACE);
  63. },
  64. handleEvent: function(aEvent) {
  65. if (aEvent.type != "AppCommand")
  66. return;
  67. aEvent.stopPropagation();
  68. switch (aEvent.command) {
  69. case "Back":
  70. if (this._backHistory.length > 0)
  71. this.back();
  72. break;
  73. case "Forward":
  74. if (this._forwardHistory.length > 0)
  75. this.forward();
  76. break;
  77. case "Search":
  78. PlacesSearchBox.findAll();
  79. break;
  80. }
  81. },
  82. destroy: function() {
  83. },
  84. _location: null,
  85. get location() {
  86. return this._location;
  87. },
  88. set location(aLocation) {
  89. if (!aLocation || this._location == aLocation)
  90. return aLocation;
  91. if (this.location) {
  92. this._backHistory.unshift(this.location);
  93. this._forwardHistory.splice(0, this._forwardHistory.length);
  94. }
  95. this._location = aLocation;
  96. this._places.selectPlaceURI(aLocation);
  97. if (!this._places.hasSelection) {
  98. // If no node was found for the given place: uri, just load it directly
  99. ContentArea.currentPlace = aLocation;
  100. }
  101. this.updateDetailsPane();
  102. // update navigation commands
  103. if (this._backHistory.length == 0)
  104. document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
  105. else
  106. document.getElementById("OrganizerCommand:Back").removeAttribute("disabled");
  107. if (this._forwardHistory.length == 0)
  108. document.getElementById("OrganizerCommand:Forward").setAttribute("disabled", true);
  109. else
  110. document.getElementById("OrganizerCommand:Forward").removeAttribute("disabled");
  111. return aLocation;
  112. },
  113. _backHistory: [],
  114. _forwardHistory: [],
  115. back: function() {
  116. this._forwardHistory.unshift(this.location);
  117. var historyEntry = this._backHistory.shift();
  118. this._location = null;
  119. this.location = historyEntry;
  120. },
  121. forward: function() {
  122. this._backHistory.unshift(this.location);
  123. var historyEntry = this._forwardHistory.shift();
  124. this._location = null;
  125. this.location = historyEntry;
  126. },
  127. /**
  128. * Called when a place folder is selected in the left pane.
  129. * @param resetSearchBox
  130. * true if the search box should also be reset, false otherwise.
  131. * The search box should be reset when a new folder in the left
  132. * pane is selected; the search scope and text need to be cleared in
  133. * preparation for the new folder. Note that if the user manually
  134. * resets the search box, either by clicking its reset button or by
  135. * deleting its text, this will be false.
  136. */
  137. _cachedLeftPaneSelectedURI: null,
  138. onPlaceSelected: function(resetSearchBox) {
  139. // Don't change the right-hand pane contents when there's no selection.
  140. if (!this._places.hasSelection)
  141. return;
  142. var node = this._places.selectedNode;
  143. var queries = PlacesUtils.asQuery(node).getQueries();
  144. // Items are only excluded on the left pane.
  145. var options = node.queryOptions.clone();
  146. options.excludeItems = false;
  147. var placeURI = PlacesUtils.history.queriesToQueryString(queries,
  148. queries.length,
  149. options);
  150. // If either the place of the content tree in the right pane has changed or
  151. // the user cleared the search box, update the place, hide the search UI,
  152. // and update the back/forward buttons by setting location.
  153. if (ContentArea.currentPlace != placeURI || !resetSearchBox) {
  154. ContentArea.currentPlace = placeURI;
  155. PlacesSearchBox.hideSearchUI();
  156. this.location = node.uri;
  157. }
  158. // Update the selected folder title where it appears in the UI: the folder
  159. // scope button, and the search box emptytext.
  160. // They must be updated even if the selection hasn't changed --
  161. // specifically when node's title changes. In that case a selection event
  162. // is generated, this method is called, but the selection does not change.
  163. var folderButton = document.getElementById("scopeBarFolder");
  164. var folderTitle = node.title || folderButton.getAttribute("emptytitle");
  165. folderButton.setAttribute("label", folderTitle);
  166. if (PlacesSearchBox.filterCollection == "collection")
  167. PlacesSearchBox.updateCollectionTitle(folderTitle);
  168. // When we invalidate a container we use suppressSelectionEvent, when it is
  169. // unset a select event is fired, in many cases the selection did not really
  170. // change, so we should check for it, and return early in such a case. Note
  171. // that we cannot return any earlier than this point, because when
  172. // !resetSearchBox, we need to update location and hide the UI as above,
  173. // even though the selection has not changed.
  174. if (node.uri == this._cachedLeftPaneSelectedURI)
  175. return;
  176. this._cachedLeftPaneSelectedURI = node.uri;
  177. // At this point, resetSearchBox is true, because the left pane selection
  178. // has changed; otherwise we would have returned earlier.
  179. PlacesSearchBox.searchFilter.reset();
  180. this._setSearchScopeForNode(node);
  181. this.updateDetailsPane();
  182. },
  183. /**
  184. * Sets the search scope based on aNode's properties.
  185. * @param aNode
  186. * the node to set up scope from
  187. */
  188. _setSearchScopeForNode: function(aNode) {
  189. let itemId = aNode.itemId;
  190. // Set default buttons status.
  191. let bookmarksButton = document.getElementById("scopeBarAll");
  192. bookmarksButton.hidden = false;
  193. let downloadsButton = document.getElementById("scopeBarDownloads");
  194. downloadsButton.hidden = true;
  195. if (PlacesUtils.nodeIsHistoryContainer(aNode) ||
  196. itemId == PlacesUIUtils.leftPaneQueries["History"]) {
  197. PlacesQueryBuilder.setScope("history");
  198. }
  199. else if (itemId == PlacesUIUtils.leftPaneQueries["Downloads"]) {
  200. downloadsButton.hidden = false;
  201. bookmarksButton.hidden = true;
  202. PlacesQueryBuilder.setScope("downloads");
  203. }
  204. else {
  205. // Default to All Bookmarks for all other nodes, per bug 469437.
  206. PlacesQueryBuilder.setScope("bookmarks");
  207. }
  208. // Enable or disable the folder scope button.
  209. let folderButton = document.getElementById("scopeBarFolder");
  210. folderButton.hidden = !PlacesUtils.nodeIsFolder(aNode) ||
  211. itemId == PlacesUIUtils.allBookmarksFolderId;
  212. },
  213. /**
  214. * Handle clicks on the places list.
  215. * Single Left click, right click or modified click do not result in any
  216. * special action, since they're related to selection.
  217. * @param aEvent
  218. * The mouse event.
  219. */
  220. onPlacesListClick: function(aEvent) {
  221. // Only handle clicks on tree children.
  222. if (aEvent.target.localName != "treechildren")
  223. return;
  224. let node = this._places.selectedNode;
  225. if (node) {
  226. let middleClick = aEvent.button == 1 && aEvent.detail == 1;
  227. if (middleClick && PlacesUtils.nodeIsContainer(node)) {
  228. // The command execution function will take care of seeing if the
  229. // selection is a folder or a different container type, and will
  230. // load its contents in tabs.
  231. PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent, this._places);
  232. }
  233. }
  234. },
  235. /**
  236. * Handle focus changes on the places list and the current content view.
  237. */
  238. updateDetailsPane: function() {
  239. if (!ContentArea.currentViewOptions.showDetailsPane)
  240. return;
  241. let view = PlacesUIUtils.getViewForNode(document.activeElement);
  242. if (view) {
  243. let selectedNodes = view.selectedNode ?
  244. [view.selectedNode] : view.selectedNodes;
  245. this._fillDetailsPane(selectedNodes);
  246. }
  247. },
  248. openFlatContainer: function(aContainer) {
  249. if (aContainer.itemId != -1)
  250. this._places.selectItems([aContainer.itemId]);
  251. else if (PlacesUtils.nodeIsQuery(aContainer))
  252. this._places.selectPlaceURI(aContainer.uri);
  253. },
  254. /**
  255. * Returns the options associated with the query currently loaded in the
  256. * main places pane.
  257. */
  258. getCurrentOptions: function() {
  259. return PlacesUtils.asQuery(ContentArea.currentView.result.root).queryOptions;
  260. },
  261. /**
  262. * Returns the queries associated with the query currently loaded in the
  263. * main places pane.
  264. */
  265. getCurrentQueries: function() {
  266. return PlacesUtils.asQuery(ContentArea.currentView.result.root).getQueries();
  267. },
  268. /**
  269. * Open a file-picker and import the selected file into the bookmarks store
  270. */
  271. importFromFile: function() {
  272. let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  273. let fpCallback = function fpCallback_done(aResult) {
  274. if (aResult != Ci.nsIFilePicker.returnCancel && fp.fileURL) {
  275. Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
  276. BookmarkHTMLUtils.importFromURL(fp.fileURL.spec, false)
  277. .then(null, Components.utils.reportError);
  278. }
  279. };
  280. fp.init(window, PlacesUIUtils.getString("SelectImport"),
  281. Ci.nsIFilePicker.modeOpen);
  282. fp.appendFilters(Ci.nsIFilePicker.filterHTML);
  283. fp.open(fpCallback);
  284. },
  285. /**
  286. * Allows simple exporting of bookmarks.
  287. */
  288. exportBookmarks: function() {
  289. let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  290. let fpCallback = function fpCallback_done(aResult) {
  291. if (aResult != Ci.nsIFilePicker.returnCancel) {
  292. Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
  293. BookmarkHTMLUtils.exportToFile(fp.file.path)
  294. .then(null, Components.utils.reportError);
  295. }
  296. };
  297. fp.init(window, PlacesUIUtils.getString("EnterExport"),
  298. Ci.nsIFilePicker.modeSave);
  299. fp.appendFilters(Ci.nsIFilePicker.filterHTML);
  300. fp.defaultString = "bookmarks.html";
  301. fp.open(fpCallback);
  302. },
  303. /**
  304. * Populates the restore menu with the dates of the backups available.
  305. */
  306. populateRestoreMenu: function() {
  307. let restorePopup = document.getElementById("fileRestorePopup");
  308. let dateSvc = Cc["@mozilla.org/intl/scriptabledateformat;1"].
  309. getService(Ci.nsIScriptableDateFormat);
  310. // Remove existing menu items. Last item is the restoreFromFile item.
  311. while (restorePopup.childNodes.length > 1)
  312. restorePopup.removeChild(restorePopup.firstChild);
  313. Task.spawn(function() {
  314. let backupFiles = yield PlacesBackups.getBackupFiles();
  315. if (backupFiles.length == 0)
  316. return;
  317. // Populate menu with backups.
  318. for (let i = 0; i < backupFiles.length; i++) {
  319. let fileSize = (yield OS.File.stat(backupFiles[i])).size;
  320. let [size, unit] = DownloadUtils.convertByteUnits(fileSize);
  321. let sizeString = PlacesUtils.getFormattedString("backupFileSizeText",
  322. [size, unit]);
  323. let sizeInfo;
  324. let bookmarkCount = PlacesBackups.getBookmarkCountForFile(backupFiles[i]);
  325. if (bookmarkCount != null) {
  326. sizeInfo = " (" + sizeString + " - " +
  327. PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
  328. bookmarkCount,
  329. [bookmarkCount]) +
  330. ")";
  331. } else {
  332. sizeInfo = " (" + sizeString + ")";
  333. }
  334. let backupDate = PlacesBackups.getDateForFile(backupFiles[i]);
  335. let m = restorePopup.insertBefore(document.createElement("menuitem"),
  336. document.getElementById("restoreFromFile"));
  337. m.setAttribute("label",
  338. dateSvc.FormatDate("",
  339. Ci.nsIScriptableDateFormat.dateFormatLong,
  340. backupDate.getFullYear(),
  341. backupDate.getMonth() + 1,
  342. backupDate.getDate()) +
  343. sizeInfo);
  344. m.setAttribute("value", OS.Path.basename(backupFiles[i]));
  345. m.setAttribute("oncommand",
  346. "PlacesOrganizer.onRestoreMenuItemClick(this);");
  347. }
  348. // Add the restoreFromFile item.
  349. restorePopup.insertBefore(document.createElement("menuseparator"),
  350. document.getElementById("restoreFromFile"));
  351. });
  352. },
  353. /**
  354. * Called when a menuitem is selected from the restore menu.
  355. */
  356. onRestoreMenuItemClick: function(aMenuItem) {
  357. Task.spawn(function() {
  358. let backupName = aMenuItem.getAttribute("value");
  359. let backupFilePaths = yield PlacesBackups.getBackupFiles();
  360. for (let backupFilePath of backupFilePaths) {
  361. if (OS.Path.basename(backupFilePath) == backupName) {
  362. PlacesOrganizer.restoreBookmarksFromFile(new FileUtils.File(backupFilePath));
  363. break;
  364. }
  365. }
  366. });
  367. },
  368. /**
  369. * Called when 'Choose File...' is selected from the restore menu.
  370. * Prompts for a file and restores bookmarks to those in the file.
  371. */
  372. onRestoreBookmarksFromFile: function() {
  373. let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
  374. getService(Ci.nsIProperties);
  375. let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
  376. let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  377. let fpCallback = function fpCallback_done(aResult) {
  378. if (aResult != Ci.nsIFilePicker.returnCancel) {
  379. this.restoreBookmarksFromFile(fp.file);
  380. }
  381. }.bind(this);
  382. fp.init(window, PlacesUIUtils.getString("bookmarksRestoreTitle"),
  383. Ci.nsIFilePicker.modeOpen);
  384. fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
  385. RESTORE_FILEPICKER_FILTER_EXT);
  386. fp.appendFilters(Ci.nsIFilePicker.filterAll);
  387. fp.displayDirectory = backupsDir;
  388. fp.open(fpCallback);
  389. },
  390. /**
  391. * Restores bookmarks from a JSON file.
  392. */
  393. restoreBookmarksFromFile: function(aFile) {
  394. // check file extension
  395. let filePath = aFile.path;
  396. if (!filePath.toLowerCase().endsWith("json") &&
  397. !filePath.toLowerCase().endsWith("jsonlz4")) {
  398. this._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreFormatError"));
  399. return;
  400. }
  401. // confirm ok to delete existing bookmarks
  402. var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"].
  403. getService(Ci.nsIPromptService);
  404. if (!prompts.confirm(null,
  405. PlacesUIUtils.getString("bookmarksRestoreAlertTitle"),
  406. PlacesUIUtils.getString("bookmarksRestoreAlert")))
  407. return;
  408. Task.spawn(function() {
  409. try {
  410. yield BookmarkJSONUtils.importFromFile(aFile.path, true);
  411. } catch(ex) {
  412. PlacesOrganizer._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreParseError"));
  413. }
  414. });
  415. },
  416. _showErrorAlert: function(aMsg) {
  417. var brandShortName = document.getElementById("brandStrings").
  418. getString("brandShortName");
  419. Cc["@mozilla.org/embedcomp/prompt-service;1"].
  420. getService(Ci.nsIPromptService).
  421. alert(window, brandShortName, aMsg);
  422. },
  423. /**
  424. * Backup bookmarks to desktop, auto-generate a filename with a date.
  425. * The file is a JSON serialization of bookmarks, tags and any annotations
  426. * of those items.
  427. */
  428. backupBookmarks: function() {
  429. let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
  430. getService(Ci.nsIProperties);
  431. let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
  432. let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  433. let fpCallback = function fpCallback_done(aResult) {
  434. if (aResult != Ci.nsIFilePicker.returnCancel) {
  435. BookmarkJSONUtils.exportToFile(fp.file.path);
  436. }
  437. };
  438. fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"),
  439. Ci.nsIFilePicker.modeSave);
  440. fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
  441. RESTORE_FILEPICKER_FILTER_EXT);
  442. fp.defaultString = PlacesBackups.getFilenameForDate();
  443. fp.displayDirectory = backupsDir;
  444. fp.open(fpCallback);
  445. },
  446. _paneDisabled: false,
  447. _setDetailsFieldsDisabledState:
  448. function(aDisabled) {
  449. if (aDisabled) {
  450. document.getElementById("paneElementsBroadcaster")
  451. .setAttribute("disabled", "true");
  452. }
  453. else {
  454. document.getElementById("paneElementsBroadcaster")
  455. .removeAttribute("disabled");
  456. }
  457. },
  458. _detectAndSetDetailsPaneMinimalState:
  459. function(aNode) {
  460. /**
  461. * The details of simple folder-items (as opposed to livemarks) or the
  462. * of livemark-children are not likely to fill the infoBox anyway,
  463. * thus we remove the "More/Less" button and show all details.
  464. *
  465. * the wasminimal attribute here is used to persist the "more/less"
  466. * state in a bookmark->folder->bookmark scenario.
  467. */
  468. var infoBox = document.getElementById("infoBox");
  469. var infoBoxExpander = document.getElementById("infoBoxExpander");
  470. var infoBoxExpanderWrapper = document.getElementById("infoBoxExpanderWrapper");
  471. var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster");
  472. if (!aNode) {
  473. infoBoxExpanderWrapper.hidden = true;
  474. return;
  475. }
  476. if (aNode.itemId != -1 &&
  477. PlacesUtils.nodeIsFolder(aNode) && !aNode._feedURI) {
  478. if (infoBox.getAttribute("minimal") == "true")
  479. infoBox.setAttribute("wasminimal", "true");
  480. infoBox.removeAttribute("minimal");
  481. infoBoxExpanderWrapper.hidden = true;
  482. }
  483. else {
  484. if (infoBox.getAttribute("wasminimal") == "true")
  485. infoBox.setAttribute("minimal", "true");
  486. infoBox.removeAttribute("wasminimal");
  487. infoBoxExpanderWrapper.hidden =
  488. this._additionalInfoFields.every(function(id)
  489. document.getElementById(id).collapsed);
  490. }
  491. additionalInfoBroadcaster.hidden = infoBox.getAttribute("minimal") == "true";
  492. },
  493. // NOT YET USED
  494. updateThumbnailProportions: function() {
  495. var previewBox = document.getElementById("previewBox");
  496. var canvas = document.getElementById("itemThumbnail");
  497. var height = previewBox.boxObject.height;
  498. var width = height * (screen.width / screen.height);
  499. canvas.width = width;
  500. canvas.height = height;
  501. },
  502. _fillDetailsPane: function(aNodeList) {
  503. var infoBox = document.getElementById("infoBox");
  504. var detailsDeck = document.getElementById("detailsDeck");
  505. // Make sure the infoBox UI is visible if we need to use it, we hide it
  506. // below when we don't.
  507. infoBox.hidden = false;
  508. var aSelectedNode = aNodeList.length == 1 ? aNodeList[0] : null;
  509. // If a textbox within a panel is focused, force-blur it so its contents
  510. // are saved
  511. if (gEditItemOverlay.itemId != -1) {
  512. var focusedElement = document.commandDispatcher.focusedElement;
  513. if ((focusedElement instanceof HTMLInputElement ||
  514. focusedElement instanceof HTMLTextAreaElement) &&
  515. /^editBMPanel.*/.test(focusedElement.parentNode.parentNode.id))
  516. focusedElement.blur();
  517. // don't update the panel if we are already editing this node unless we're
  518. // in multi-edit mode
  519. if (aSelectedNode) {
  520. var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode);
  521. var nodeIsSame = gEditItemOverlay.itemId == aSelectedNode.itemId ||
  522. gEditItemOverlay.itemId == concreteId ||
  523. (aSelectedNode.itemId == -1 && gEditItemOverlay.uri &&
  524. gEditItemOverlay.uri == aSelectedNode.uri);
  525. if (nodeIsSame && detailsDeck.selectedIndex == 1 &&
  526. !gEditItemOverlay.multiEdit)
  527. return;
  528. }
  529. }
  530. // Clean up the panel before initing it again.
  531. gEditItemOverlay.uninitPanel(false);
  532. if (aSelectedNode && !PlacesUtils.nodeIsSeparator(aSelectedNode)) {
  533. detailsDeck.selectedIndex = 1;
  534. // Using the concrete itemId is arguably wrong. The bookmarks API
  535. // does allow setting properties for folder shortcuts as well, but since
  536. // the UI does not distinct between the couple, we better just show
  537. // the concrete item properties for shortcuts to root nodes.
  538. var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode);
  539. var isRootItem = concreteId != -1 && PlacesUtils.isRootItem(concreteId);
  540. var readOnly = isRootItem ||
  541. aSelectedNode.parent.itemId == PlacesUIUtils.leftPaneFolderId;
  542. var useConcreteId = isRootItem ||
  543. PlacesUtils.nodeIsTagQuery(aSelectedNode);
  544. var itemId = -1;
  545. if (concreteId != -1 && useConcreteId)
  546. itemId = concreteId;
  547. else if (aSelectedNode.itemId != -1)
  548. itemId = aSelectedNode.itemId;
  549. else
  550. itemId = PlacesUtils._uri(aSelectedNode.uri);
  551. gEditItemOverlay.initPanel(itemId, { hiddenRows: ["folderPicker"]
  552. , forceReadOnly: readOnly
  553. , titleOverride: aSelectedNode.title
  554. });
  555. // Dynamically generated queries, like history date containers, have
  556. // itemId !=0 and do not exist in history. For them the panel is
  557. // read-only, but empty, since it can't get a valid title for the object.
  558. // In such a case we force the title using the selectedNode one, for UI
  559. // polishness.
  560. if (aSelectedNode.itemId == -1 &&
  561. (PlacesUtils.nodeIsDay(aSelectedNode) ||
  562. PlacesUtils.nodeIsHost(aSelectedNode)))
  563. gEditItemOverlay._element("namePicker").value = aSelectedNode.title;
  564. this._detectAndSetDetailsPaneMinimalState(aSelectedNode);
  565. }
  566. else if (!aSelectedNode && aNodeList[0]) {
  567. var itemIds = [];
  568. for (var i = 0; i < aNodeList.length; i++) {
  569. if (!PlacesUtils.nodeIsBookmark(aNodeList[i]) &&
  570. !PlacesUtils.nodeIsURI(aNodeList[i])) {
  571. detailsDeck.selectedIndex = 0;
  572. var selectItemDesc = document.getElementById("selectItemDescription");
  573. var itemsCountLabel = document.getElementById("itemsCountText");
  574. selectItemDesc.hidden = false;
  575. itemsCountLabel.value =
  576. PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
  577. aNodeList.length, [aNodeList.length]);
  578. infoBox.hidden = true;
  579. return;
  580. }
  581. itemIds[i] = aNodeList[i].itemId != -1 ? aNodeList[i].itemId :
  582. PlacesUtils._uri(aNodeList[i].uri);
  583. }
  584. detailsDeck.selectedIndex = 1;
  585. gEditItemOverlay.initPanel(itemIds,
  586. { hiddenRows: ["folderPicker",
  587. "loadInSidebar",
  588. "location",
  589. "keyword",
  590. "description",
  591. "name"]});
  592. this._detectAndSetDetailsPaneMinimalState(aSelectedNode);
  593. }
  594. else {
  595. detailsDeck.selectedIndex = 0;
  596. infoBox.hidden = true;
  597. let selectItemDesc = document.getElementById("selectItemDescription");
  598. let itemsCountLabel = document.getElementById("itemsCountText");
  599. let itemsCount = 0;
  600. if (ContentArea.currentView.result) {
  601. let rootNode = ContentArea.currentView.result.root;
  602. if (rootNode.containerOpen)
  603. itemsCount = rootNode.childCount;
  604. }
  605. if (itemsCount == 0) {
  606. selectItemDesc.hidden = true;
  607. itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.noItems");
  608. }
  609. else {
  610. selectItemDesc.hidden = false;
  611. itemsCountLabel.value =
  612. PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
  613. itemsCount, [itemsCount]);
  614. }
  615. }
  616. },
  617. // NOT YET USED
  618. _updateThumbnail: function() {
  619. var bo = document.getElementById("previewBox").boxObject;
  620. var width = bo.width;
  621. var height = bo.height;
  622. var canvas = document.getElementById("itemThumbnail");
  623. var ctx = canvas.getContext('2d');
  624. var notAvailableText = canvas.getAttribute("notavailabletext");
  625. ctx.save();
  626. ctx.fillStyle = "-moz-Dialog";
  627. ctx.fillRect(0, 0, width, height);
  628. ctx.translate(width/2, height/2);
  629. ctx.fillStyle = "GrayText";
  630. ctx.mozTextStyle = "12pt sans serif";
  631. var len = ctx.mozMeasureText(notAvailableText);
  632. ctx.translate(-len/2,0);
  633. ctx.mozDrawText(notAvailableText);
  634. ctx.restore();
  635. },
  636. toggleAdditionalInfoFields: function() {
  637. var infoBox = document.getElementById("infoBox");
  638. var infoBoxExpander = document.getElementById("infoBoxExpander");
  639. var infoBoxExpanderLabel = document.getElementById("infoBoxExpanderLabel");
  640. var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster");
  641. if (infoBox.getAttribute("minimal") == "true") {
  642. infoBox.removeAttribute("minimal");
  643. infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("lesslabel");
  644. infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("lessaccesskey");
  645. infoBoxExpander.className = "expander-up";
  646. additionalInfoBroadcaster.removeAttribute("hidden");
  647. }
  648. else {
  649. infoBox.setAttribute("minimal", "true");
  650. infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("morelabel");
  651. infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("moreaccesskey");
  652. infoBoxExpander.className = "expander-down";
  653. additionalInfoBroadcaster.setAttribute("hidden", "true");
  654. }
  655. },
  656. /**
  657. * Save the current search (or advanced query) to the bookmarks root.
  658. */
  659. saveSearch: function() {
  660. // Get the place: uri for the query.
  661. // If the advanced query builder is showing, use that.
  662. var options = this.getCurrentOptions();
  663. var queries = this.getCurrentQueries();
  664. var placeSpec = PlacesUtils.history.queriesToQueryString(queries,
  665. queries.length,
  666. options);
  667. var placeURI = Cc["@mozilla.org/network/io-service;1"].
  668. getService(Ci.nsIIOService).
  669. newURI(placeSpec, null, null);
  670. // Prompt the user for a name for the query.
  671. // XXX - using prompt service for now; will need to make
  672. // a real dialog and localize when we're sure this is the UI we want.
  673. var title = PlacesUIUtils.getString("saveSearch.title");
  674. var inputLabel = PlacesUIUtils.getString("saveSearch.inputLabel");
  675. var defaultText = PlacesUIUtils.getString("saveSearch.inputDefaultText");
  676. var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"].
  677. getService(Ci.nsIPromptService);
  678. var check = {value: false};
  679. var input = {value: defaultText};
  680. var save = prompts.prompt(null, title, inputLabel, input, null, check);
  681. // Don't add the query if the user cancels or clears the seach name.
  682. if (!save || input.value == "")
  683. return;
  684. // Add the place: uri as a bookmark under the bookmarks root.
  685. var txn = new PlacesCreateBookmarkTransaction(placeURI,
  686. PlacesUtils.bookmarksMenuFolderId,
  687. PlacesUtils.bookmarks.DEFAULT_INDEX,
  688. input.value);
  689. PlacesUtils.transactionManager.doTransaction(txn);
  690. // select and load the new query
  691. this._places.selectPlaceURI(placeSpec);
  692. }
  693. };
  694. /**
  695. * A set of utilities relating to search within Bookmarks and History.
  696. */
  697. var PlacesSearchBox = {
  698. /**
  699. * The Search text field
  700. */
  701. get searchFilter() {
  702. return document.getElementById("searchFilter");
  703. },
  704. /**
  705. * Folders to include when searching.
  706. */
  707. _folders: [],
  708. get folders() {
  709. if (this._folders.length == 0) {
  710. this._folders.push(PlacesUtils.bookmarksMenuFolderId,
  711. PlacesUtils.unfiledBookmarksFolderId,
  712. PlacesUtils.toolbarFolderId);
  713. }
  714. return this._folders;
  715. },
  716. set folders(aFolders) {
  717. this._folders = aFolders;
  718. return aFolders;
  719. },
  720. /**
  721. * Run a search for the specified text, over the collection specified by
  722. * the dropdown arrow. The default is all bookmarks, but can be
  723. * localized to the active collection.
  724. * @param filterString
  725. * The text to search for.
  726. */
  727. search: function(filterString) {
  728. var PO = PlacesOrganizer;
  729. // If the user empties the search box manually, reset it and load all
  730. // contents of the current scope.
  731. // XXX this might be to jumpy, maybe should search for "", so results
  732. // are ungrouped, and search box not reset
  733. if (filterString == "") {
  734. PO.onPlaceSelected(false);
  735. return;
  736. }
  737. let currentView = ContentArea.currentView;
  738. let currentOptions = PO.getCurrentOptions();
  739. // Search according to the current scope and folders, which were set by
  740. // PQB_setScope()
  741. switch (PlacesSearchBox.filterCollection) {
  742. case "collection":
  743. currentView.applyFilter(filterString, this.folders);
  744. break;
  745. case "bookmarks":
  746. currentView.applyFilter(filterString, this.folders);
  747. break;
  748. case "history":
  749. if (currentOptions.queryType != Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
  750. var query = PlacesUtils.history.getNewQuery();
  751. query.searchTerms = filterString;
  752. var options = currentOptions.clone();
  753. // Make sure we're getting uri results.
  754. options.resultType = currentOptions.RESULTS_AS_URI;
  755. options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
  756. options.includeHidden = true;
  757. currentView.load([query], options);
  758. }
  759. else {
  760. currentView.applyFilter(filterString, null, true);
  761. }
  762. break;
  763. case "downloads":
  764. if (currentView == ContentTree.view) {
  765. let query = PlacesUtils.history.getNewQuery();
  766. query.searchTerms = filterString;
  767. query.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], 1);
  768. let options = currentOptions.clone();
  769. // Make sure we're getting uri results.
  770. options.resultType = currentOptions.RESULTS_AS_URI;
  771. options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
  772. options.includeHidden = true;
  773. currentView.load([query], options);
  774. }
  775. else {
  776. // The new downloads view doesn't use places for searching downloads.
  777. currentView.searchTerm = filterString;
  778. }
  779. break;
  780. default:
  781. throw new Components.Exception("Invalid filterCollection on search",
  782. Components.results.NS_ERROR_INVALID_ARG);
  783. }
  784. PlacesSearchBox.showSearchUI();
  785. // Update the details panel
  786. PlacesOrganizer.updateDetailsPane();
  787. },
  788. /**
  789. * Finds across all history, downloads or all bookmarks.
  790. */
  791. findAll: function() {
  792. switch (this.filterCollection) {
  793. case "history":
  794. PlacesQueryBuilder.setScope("history");
  795. break;
  796. case "downloads":
  797. PlacesQueryBuilder.setScope("downloads");
  798. break;
  799. default:
  800. PlacesQueryBuilder.setScope("bookmarks");
  801. break;
  802. }
  803. this.focus();
  804. },
  805. /**
  806. * Updates the display with the title of the current collection.
  807. * @param aTitle
  808. * The title of the current collection.
  809. */
  810. updateCollectionTitle: function(aTitle) {
  811. let title = "";
  812. // This is needed when a user performs a folder-specific search
  813. // using the scope bar, removes the search-string, and unfocuses
  814. // the search box, at least until the removal of the scope bar.
  815. if (aTitle) {
  816. title = PlacesUIUtils.getFormattedString("searchCurrentDefault",
  817. [aTitle]);
  818. }
  819. else {
  820. switch (this.filterCollection) {
  821. case "history":
  822. title = PlacesUIUtils.getString("searchHistory");
  823. break;
  824. case "downloads":
  825. title = PlacesUIUtils.getString("searchDownloads");
  826. break;
  827. default:
  828. title = PlacesUIUtils.getString("searchBookmarks");
  829. }
  830. }
  831. this.searchFilter.placeholder = title;
  832. },
  833. /**
  834. * Gets/sets the active collection from the dropdown menu.
  835. */
  836. get filterCollection() {
  837. return this.searchFilter.getAttribute("collection");
  838. },
  839. set filterCollection(collectionName) {
  840. if (collectionName == this.filterCollection)
  841. return collectionName;
  842. this.searchFilter.setAttribute("collection", collectionName);
  843. var newGrayText = null;
  844. if (collectionName == "collection") {
  845. newGrayText = PlacesOrganizer._places.selectedNode.title ||
  846. document.getElementById("scopeBarFolder").
  847. getAttribute("emptytitle");
  848. }
  849. this.updateCollectionTitle(newGrayText);
  850. return collectionName;
  851. },
  852. /**
  853. * Focus the search box
  854. */
  855. focus: function() {
  856. this.searchFilter.focus();
  857. },
  858. /**
  859. * Set up the gray text in the search bar as the Places View loads.
  860. */
  861. init: function() {
  862. this.updateCollectionTitle();
  863. },
  864. /**
  865. * Gets or sets the text shown in the Places Search Box
  866. */
  867. get value() {
  868. return this.searchFilter.value;
  869. },
  870. set value(value) {
  871. return this.searchFilter.value = value;
  872. },
  873. showSearchUI: function() {
  874. // Hide the advanced search controls when the user hasn't searched
  875. var searchModifiers = document.getElementById("searchModifiers");
  876. searchModifiers.hidden = false;
  877. },
  878. hideSearchUI: function() {
  879. var searchModifiers = document.getElementById("searchModifiers");
  880. searchModifiers.hidden = true;
  881. }
  882. };
  883. /**
  884. * Functions and data for advanced query builder
  885. */
  886. var PlacesQueryBuilder = {
  887. queries: [],
  888. queryOptions: null,
  889. /**
  890. * Called when a scope button in the scope bar is clicked.
  891. * @param aButton
  892. * the scope button that was selected
  893. */
  894. onScopeSelected: function(aButton) {
  895. switch (aButton.id) {
  896. case "scopeBarHistory":
  897. this.setScope("history");
  898. break;
  899. case "scopeBarFolder":
  900. this.setScope("collection");
  901. break;
  902. case "scopeBarDownloads":
  903. this.setScope("downloads");
  904. break;
  905. case "scopeBarAll":
  906. this.setScope("bookmarks");
  907. break;
  908. default:
  909. throw new Components.Exception("Invalid search scope button ID",
  910. Components.results.NS_ERROR_INVALID_ARG);
  911. break;
  912. }
  913. },
  914. /**
  915. * Sets the search scope. This can be called when no search is active, and
  916. * in that case, when the user does begin a search aScope will be used (see
  917. * PSB_search()). If there is an active search, it's performed again to
  918. * update the content tree.
  919. * @param aScope
  920. * The search scope: "bookmarks", "collection", "downloads" or
  921. * "history".
  922. */
  923. setScope: function(aScope) {
  924. // Determine filterCollection, folders, and scopeButtonId based on aScope.
  925. var filterCollection;
  926. var folders = [];
  927. var scopeButtonId;
  928. switch (aScope) {
  929. case "history":
  930. filterCollection = "history";
  931. scopeButtonId = "scopeBarHistory";
  932. break;
  933. case "collection":
  934. // The folder scope button can only become hidden upon selecting a new
  935. // folder in the left pane, and the disabled state will remain unchanged
  936. // until a new folder is selected. See PO__setScopeForNode().
  937. if (!document.getElementById("scopeBarFolder").hidden) {
  938. filterCollection = "collection";
  939. scopeButtonId = "scopeBarFolder";
  940. folders.push(PlacesUtils.getConcreteItemId(
  941. PlacesOrganizer._places.selectedNode));
  942. break;
  943. }
  944. // Fall through. If collection scope doesn't make sense for the
  945. // selected node, choose bookmarks scope.
  946. case "bookmarks":
  947. filterCollection = "bookmarks";
  948. scopeButtonId = "scopeBarAll";
  949. folders.push(PlacesUtils.bookmarksMenuFolderId,
  950. PlacesUtils.toolbarFolderId,
  951. PlacesUtils.unfiledBookmarksFolderId);
  952. break;
  953. case "downloads":
  954. filterCollection = "downloads";
  955. scopeButtonId = "scopeBarDownloads";
  956. break;
  957. default:
  958. throw new Components.Exception("Invalid search scope",
  959. Components.results.NS_ERROR_INVALID_ARG);
  960. break;
  961. }
  962. // Check the appropriate scope button in the scope bar.
  963. document.getElementById(scopeButtonId).checked = true;
  964. // Update the search box. Re-search if there's an active search.
  965. PlacesSearchBox.filterCollection = filterCollection;
  966. PlacesSearchBox.folders = folders;
  967. var searchStr = PlacesSearchBox.searchFilter.value;
  968. if (searchStr)
  969. PlacesSearchBox.search(searchStr);
  970. }
  971. };
  972. /**
  973. * Population and commands for the View Menu.
  974. */
  975. var ViewMenu = {
  976. /**
  977. * Removes content generated previously from a menupopup.
  978. * @param popup
  979. * The popup that contains the previously generated content.
  980. * @param startID
  981. * The id attribute of an element that is the start of the
  982. * dynamically generated region - remove elements after this
  983. * item only.
  984. * Must be contained by popup. Can be null (in which case the
  985. * contents of popup are removed).
  986. * @param endID
  987. * The id attribute of an element that is the end of the
  988. * dynamically generated region - remove elements up to this
  989. * item only.
  990. * Must be contained by popup. Can be null (in which case all
  991. * items until the end of the popup will be removed). Ignored
  992. * if startID is null.
  993. * @returns The element for the caller to insert new items before,
  994. * null if the caller should just append to the popup.
  995. */
  996. _clean: function(popup, startID, endID) {
  997. if (endID)
  998. NS_ASSERT(startID, "meaningless to have valid endID and null startID");
  999. if (startID) {
  1000. var startElement = document.getElementById(startID);
  1001. NS_ASSERT(startElement.parentNode ==
  1002. popup, "startElement is not in popup");
  1003. NS_ASSERT(startElement,
  1004. "startID does not correspond to an existing element");
  1005. var endElement = null;
  1006. if (endID) {
  1007. endElement = document.getElementById(endID);
  1008. NS_ASSERT(endElement.parentNode == popup,
  1009. "endElement is not in popup");
  1010. NS_ASSERT(endElement,
  1011. "endID does not correspond to an existing element");
  1012. }
  1013. while (startElement.nextSibling != endElement)
  1014. popup.removeChild(startElement.nextSibling);
  1015. return endElement;
  1016. }
  1017. else {
  1018. while(popup.hasChildNodes())
  1019. popup.removeChild(popup.firstChild);
  1020. }
  1021. return null;
  1022. },
  1023. /**
  1024. * Fills a menupopup with a list of columns
  1025. * @param event
  1026. * The popupshowing event that invoked this function.
  1027. * @param startID
  1028. * see _clean
  1029. * @param endID
  1030. * see _clean
  1031. * @param type
  1032. * the type of the menuitem, e.g. "radio" or "checkbox".
  1033. * Can be null (no-type).
  1034. * Checkboxes are checked if the column is visible.
  1035. * @param propertyPrefix
  1036. * If propertyPrefix is non-null:
  1037. * propertyPrefix + column ID + ".label" will be used to get the
  1038. * localized label string.
  1039. * propertyPrefix + column ID + ".accesskey" will be used to get the
  1040. * localized accesskey.
  1041. * If propertyPrefix is null, the column label is used as label and
  1042. * no accesskey is assigned.
  1043. */
  1044. fillWithColumns: function(event, startID, endID, type, propertyPrefix) {
  1045. var popup = event.target;
  1046. var pivot = this._clean(popup, startID, endID);
  1047. // If no column is "sort-active", the "Unsorted" item needs to be checked,
  1048. // so track whether or not we find a column that is sort-active.
  1049. var isSorted = false;
  1050. var content = document.getElementById("placeContent");
  1051. var columns = content.columns;
  1052. for (var i = 0; i < columns.count; ++i) {
  1053. var column = columns.getColumnAt(i).element;
  1054. if (popup.parentNode && (popup.parentNode.id == "viewSort")) {
  1055. switch (column.id) {
  1056. case "placesContentParentFolder":
  1057. continue;
  1058. case "placesContentParentFolderPath":
  1059. continue;
  1060. }
  1061. }
  1062. var menuitem = document.createElement("menuitem");
  1063. menuitem.id = "menucol_" + column.id;
  1064. menuitem.column = column;
  1065. var label = column.getAttribute("label");
  1066. if (propertyPrefix) {
  1067. var menuitemPrefix = propertyPrefix;
  1068. // for string properties, use "name" as the id, instead of "title"
  1069. // see bug #386287 for details
  1070. var columnId = column.getAttribute("anonid");
  1071. menuitemPrefix += columnId == "title" ? "name" : columnId;
  1072. label = PlacesUIUtils.getString(menuitemPrefix + ".label");
  1073. var accesskey = PlacesUIUtils.getString(menuitemPrefix + ".accesskey");
  1074. menuitem.setAttribute("accesskey", accesskey);
  1075. }
  1076. menuitem.setAttribute("label", label);
  1077. if (type == "radio") {
  1078. menuitem.setAttribute("type", "radio");
  1079. menuitem.setAttribute("name", "columns");
  1080. // This column is the sort key. Its item is checked.
  1081. if (column.getAttribute("sortDirection") != "") {
  1082. menuitem.setAttribute("checked", "true");
  1083. isSorted = true;
  1084. }
  1085. }
  1086. else if (type == "checkbox") {
  1087. menuitem.setAttribute("type", "checkbox");
  1088. // Cannot uncheck the primary column.
  1089. if (column.getAttribute("primary") == "true")
  1090. menuitem.setAttribute("disabled", "true");
  1091. // Items for visible columns are checked.
  1092. if (!column.hidden)
  1093. menuitem.setAttribute("checked", "true");
  1094. }
  1095. if (pivot)
  1096. popup.insertBefore(menuitem, pivot);
  1097. else
  1098. popup.appendChild(menuitem);
  1099. }
  1100. event.stopPropagation();
  1101. },
  1102. /**
  1103. * Set up the content of the view menu.
  1104. */
  1105. populateSortMenu: function(event) {
  1106. this.fillWithColumns(event, "viewUnsorted", "directionSeparator", "radio", "view.sortBy.");
  1107. var sortColumn = this._getSortColumn();
  1108. var viewSortAscending = document.getElementById("viewSortAscending");
  1109. var viewSortDescending = document.getElementById("viewSortDescending");
  1110. // We need to remove an existing checked attribute because the unsorted
  1111. // menu item is not rebuilt every time we open the menu like the others.
  1112. var viewUnsorted = document.getElementById("viewUnsorted");
  1113. if (!sortColumn) {
  1114. viewSortAscending.removeAttribute("checked");
  1115. viewSortDescending.removeAttribute("checked");
  1116. viewUnsorted.setAttribute("checked", "true");
  1117. }
  1118. else if (sortColumn.getAttribute("sortDirection") == "ascending") {
  1119. viewSortAscending.setAttribute("checked", "true");
  1120. viewSortDescending.removeAttribute("checked");
  1121. viewUnsorted.removeAttribute("checked");
  1122. }
  1123. else if (sortColumn.getAttribute("sortDirection") == "descending") {
  1124. viewSortDescending.setAttribute("checked", "true");
  1125. viewSortAscending.removeAttribute("checked");
  1126. viewUnsorted.removeAttribute("checked");
  1127. }
  1128. },
  1129. /**
  1130. * Shows/Hides a tree column.
  1131. * @param element
  1132. * The menuitem element for the column
  1133. */
  1134. showHideColumn: function(element) {
  1135. var column = element.column;
  1136. var splitter = column.nextSibling;
  1137. if (splitter && splitter.localName != "splitter")
  1138. splitter = null;
  1139. if (element.getAttribute("checked") == "true") {
  1140. column.setAttribute("hidden", "false");
  1141. if (splitter)
  1142. splitter.removeAttribute("hidden");
  1143. }
  1144. else {
  1145. column.setAttribute("hidden", "true");
  1146. if (splitter)
  1147. splitter.setAttribute("hidden", "true");
  1148. }
  1149. },
  1150. /**
  1151. * Gets the last column that was sorted.
  1152. * @returns the currently sorted column, null if there is no sorted column.
  1153. */
  1154. _getSortColumn: function() {
  1155. var content = document.getElementById("placeContent");
  1156. var cols = content.columns;
  1157. for (var i = 0; i < cols.count; ++i) {
  1158. var column = cols.getColumnAt(i).element;
  1159. var sortDirection = column.getAttribute("sortDirection");
  1160. if (sortDirection == "ascending" || sortDirection == "descending")
  1161. return column;
  1162. }
  1163. return null;
  1164. },
  1165. /**
  1166. * Sorts the view by the specified column.
  1167. * @param aColumn
  1168. * The colum that is the sort key. Can be null - the
  1169. * current sort column or the title column will be used.
  1170. * @param aDirection
  1171. * The direction to sort - "ascending" or "descending".
  1172. * Can be null - the last direction or descending will be used.
  1173. *
  1174. * If both aColumnID and aDirection are null, the view will be unsorted.
  1175. */
  1176. setSortColumn: function(aColumn, aDirection) {
  1177. var result = document.getElementById("placeContent").result;
  1178. if (!aColumn && !aDirection) {
  1179. result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
  1180. return;
  1181. }
  1182. var columnId;
  1183. if (aColumn) {
  1184. columnId = aColumn.getAttribute("anonid");
  1185. if (!aDirection) {
  1186. var sortColumn = this._getSortColumn();
  1187. if (sortColumn)
  1188. aDirection = sortColumn.getAttribute("sortDirection");
  1189. }
  1190. }
  1191. else {
  1192. var sortColumn = this._getSortColumn();
  1193. columnId = sortColumn ? sortColumn.getAttribute("anonid") : "title";
  1194. }
  1195. // This maps the possible values of columnId (i.e., anonid's of treecols in
  1196. // placeContent) to the default sortingMode and sortingAnnotation values for
  1197. // each column.
  1198. // key: Sort key in the name of one of the
  1199. // nsINavHistoryQueryOptions.SORT_BY_* constants
  1200. // dir: Default sort direction to use if none has been specified
  1201. // anno: The annotation to sort by, if key is "ANNOTATION"
  1202. var colLookupTable = {
  1203. title: { key: "TITLE", dir: "ascending" },
  1204. tags: { key: "TAGS", dir: "ascending" },
  1205. url: { key: "URI", dir: "ascending" },
  1206. date: { key: "DATE", dir: "descending" },
  1207. visitCount: { key: "VISITCOUNT", dir: "descending" },
  1208. keyword: { key: "KEYWORD", dir: "ascending" },
  1209. dateAdded: { key: "DATEADDED", dir: "descending" },
  1210. lastModified: { key: "LASTMODIFIED", dir: "descending" },
  1211. description: { key: "ANNOTATION",
  1212. dir: "ascending",
  1213. anno: PlacesUIUtils.DESCRIPTION_ANNO }
  1214. };
  1215. // Make sure we have a valid column.
  1216. if (!colLookupTable.hasOwnProperty(columnId))
  1217. throw new Components.Exception("Invalid column",
  1218. Components.results.NS_ERROR_INVALID_ARG);
  1219. // Use a default sort direction if none has been specified. If aDirection
  1220. // is invalid, result.sortingMode will be undefined, which has the effect
  1221. // of unsorting the tree.
  1222. aDirection = (aDirection || colLookupTable[columnId].dir).toUpperCase();
  1223. var sortConst = "SORT_BY_" + colLookupTable[columnId].key + "_" + aDirection;
  1224. result.sortingAnnotation = colLookupTable[columnId].anno || "";
  1225. result.sortingMode = Ci.nsINavHistoryQueryOptions[sortConst];
  1226. }
  1227. }
  1228. var ContentArea = {
  1229. _specialViews: new Map(),
  1230. init: function() {
  1231. this._deck = document.getElementById("placesViewsDeck");
  1232. this._toolbar = document.getElementById("placesToolbar");
  1233. ContentTree.init();
  1234. this._setupView();
  1235. },
  1236. /**
  1237. * Gets the content view to be used for loading the given query.
  1238. * If a custom view was set by setContentViewForQueryString, that
  1239. * view would be returned, else the default tree view is returned
  1240. *
  1241. * @param aQueryString
  1242. * a query string
  1243. * @return the view to be used for loading aQueryString.
  1244. */
  1245. getContentViewForQueryString:
  1246. function(aQueryString) {
  1247. try {
  1248. if (this._specialViews.has(aQueryString)) {
  1249. let { view, options } = this._specialViews.get(aQueryString);
  1250. if (typeof view == "function") {
  1251. view = view();
  1252. this._specialViews.set(aQueryString, { view: view, options: options });
  1253. }
  1254. return view;
  1255. }
  1256. }
  1257. catch(ex) {
  1258. Components.utils.reportError(ex);
  1259. }
  1260. return ContentTree.view;
  1261. },
  1262. /**
  1263. * Sets a custom view to be used rather than the default places tree
  1264. * whenever the given query is selected in the left pane.
  1265. * @param aQueryString
  1266. * a query string
  1267. * @param aView
  1268. * Either the custom view or a function that will return the view
  1269. * the first (and only) time it's called.
  1270. * @param [optional] aOptions
  1271. * Object defining special options for the view.
  1272. * @see ContentTree.viewOptions for supported options and default values.
  1273. */
  1274. setContentViewForQueryString:
  1275. function(aQueryString, aView, aOptions) {
  1276. if (!aQueryString ||
  1277. typeof aView != "object" && typeof aView != "function")
  1278. throw new Components.Exception("Invalid arguments",
  1279. Components.results.NS_ERROR_INVALID_ARG);
  1280. this._specialViews.set(aQueryString, { view: aView,
  1281. options: aOptions || new Object() });
  1282. },
  1283. get currentView() PlacesUIUtils.getViewForNode(this._deck.selectedPanel),
  1284. set currentView(aNewView) {
  1285. let oldView = this.currentView;
  1286. if (oldView != aNewView) {
  1287. this._deck.selectedPanel = aNewView.associatedElement;
  1288. // If the content area inactivated view was focused, move focus
  1289. // to the new view.
  1290. if (document.activeElement == oldView.associatedElement)
  1291. aNewView.associatedElement.focus();
  1292. }
  1293. return aNewView;
  1294. },
  1295. get currentPlace() this.currentView.place,
  1296. set currentPlace(aQueryString) {
  1297. let oldView = this.currentView;
  1298. let newView = this.getContentViewForQueryString(aQueryString);
  1299. newView.place = aQueryString;
  1300. if (oldView != newView) {
  1301. oldView.active = false;
  1302. this.currentView = newView;
  1303. this._setupView();
  1304. newView.active = true;
  1305. }
  1306. return aQueryString;
  1307. },
  1308. /**
  1309. * Applies view options.
  1310. */
  1311. _setupView: function() {
  1312. let options = this.currentViewOptions;
  1313. // showDetailsPane.
  1314. let detailsDeck = document.getElementById("detailsDeck");
  1315. detailsDeck.hidden = !options.showDetailsPane;
  1316. // toolbarSet.
  1317. for (let elt of this._toolbar.childNodes) {
  1318. // On Windows and Linux the menu buttons are menus wrapped in a menubar.
  1319. if (elt.id == "placesMenu") {
  1320. for (let menuElt of elt.childNodes) {
  1321. menuElt.hidden = options.toolbarSet.indexOf(menuElt.id) == -1;
  1322. }
  1323. }
  1324. else {
  1325. elt.hidden = options.toolbarSet.indexOf(elt.id) == -1;
  1326. }
  1327. }
  1328. },
  1329. /**
  1330. * Options for the current view.
  1331. *
  1332. * @see ContentTree.viewOptions for supported options and default values.
  1333. */
  1334. get currentViewOptions() {
  1335. // Use ContentTree options as default.
  1336. let viewOptions = ContentTree.viewOptions;
  1337. if (this._specialViews.has(this.currentPlace)) {
  1338. let { view, options } = this._specialViews.get(this.currentPlace);
  1339. for (let option in options) {
  1340. viewOptions[option] = options[option];
  1341. }
  1342. }
  1343. return viewOptions;
  1344. },
  1345. focus: function() {
  1346. this._deck.selectedPanel.focus();
  1347. }
  1348. };
  1349. var ContentTree = {
  1350. init: function() {
  1351. this._view = document.getElementById("placeContent");
  1352. },
  1353. get view() this._view,
  1354. get viewOptions() Object.seal({
  1355. showDetailsPane: true,
  1356. toolbarSet: "back-button, forward-button, organizeButton, viewMenu, maintenanceButton, libraryToolbarSpacer, searchFilter"
  1357. }),
  1358. openSelectedNode: function(aEvent) {
  1359. let view = this.view;
  1360. PlacesUIUtils.openNodeWithEvent(view.selectedNode, aEvent, view);
  1361. },
  1362. onClick: function(aEvent) {
  1363. let node = this.view.selectedNode;
  1364. if (node) {
  1365. let doubleClick = aEvent.button == 0 && aEvent.detail == 2;
  1366. let middleClick = aEvent.button == 1 && aEvent.detail == 1;
  1367. if (PlacesUtils.nodeIsURI(node) && (doubleClick || middleClick)) {
  1368. // Open associated uri in the browser.
  1369. this.openSelectedNode(aEvent);
  1370. }
  1371. else if (middleClick && PlacesUtils.nodeIsContainer(node)) {
  1372. // The command execution function will take care of seeing if the
  1373. // selection is a folder or a different container type, and will
  1374. // load its contents in tabs.
  1375. PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this.view);
  1376. }
  1377. }
  1378. },
  1379. onKeyPress: function(aEvent) {
  1380. if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN)
  1381. this.openSelectedNode(aEvent);
  1382. }
  1383. };