browser/components/places/content/places.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial